├── .gitignore ├── MANIFEST.in ├── README.md ├── erpnext_backup ├── __init__.py ├── config │ ├── __init__.py │ ├── desktop.py │ └── docs.py ├── erpnext_backup │ ├── __init__.py │ └── doctype │ │ ├── __init__.py │ │ └── backup_settings │ │ ├── __init__.py │ │ ├── backup_settings.js │ │ ├── backup_settings.json │ │ ├── backup_settings.py │ │ ├── test_backup_settings.js │ │ └── test_backup_settings.py ├── hooks.py ├── modules.txt ├── patches.txt └── templates │ ├── __init__.py │ └── pages │ └── __init__.py ├── license.txt ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *.egg-info 4 | *.swp 5 | tags 6 | erpnext_backup/docs/current -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in 2 | include requirements.txt 3 | include *.json 4 | include *.md 5 | include *.py 6 | include *.txt 7 | recursive-include erpnext_backup *.css 8 | recursive-include erpnext_backup *.csv 9 | recursive-include erpnext_backup *.html 10 | recursive-include erpnext_backup *.ico 11 | recursive-include erpnext_backup *.js 12 | recursive-include erpnext_backup *.json 13 | recursive-include erpnext_backup *.md 14 | recursive-include erpnext_backup *.png 15 | recursive-include erpnext_backup *.py 16 | recursive-include erpnext_backup *.svg 17 | recursive-include erpnext_backup *.txt 18 | recursive-exclude erpnext_backup *.pyc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ERPNext Backup 2 | 3 | App for auto backup of ERPNext files/database to remote server using rclone 4 | 5 | #### Pre requisite 6 | Install rclone on source computer whose backup is to be taken \ 7 | Following are steps for ubuntu \ 8 | 9 | sudo apt-get update \ 10 | sudo apt-get install zip \ 11 | curl https://rclone.org/install.sh | sudo bash \ 12 | Configure rclone for SFTP upload \ 13 | It should be run with frappe user \ 14 | https://rclone.org/sftp/ \ 15 | note the name of remote config \ 16 | n) New remote \ 17 | s) Set configuration password \ 18 | q) Quit config \ 19 | n/s/q> n \ 20 | name> remote \ 21 | rclone mkdir remote:path/to/directory \ 22 | rclone mkdir greycubelive:backup_from_sitename \ 23 | Install App \ 24 | Update Backup Settings--> RClone Remote Name--> with name noted above \ 25 | Update Backup Settings--> RClone Remote Directory Path--> with directory noted above 26 | #### License 27 | 28 | MIT 29 | 30 |
31 | 32 | #### Contact Us 33 | 34 |
35 | 1st ERPNext [Certified Partner](https://frappe.io/api/method/frappe.utils.print_format.download_pdf?doctype=Certification&name=PARTCRTF00002&format=Partner%20Certificate&no_letterhead=0&letterhead=Blank&settings=%7B%7D&_lang=en#toolbar=0) 36 | 37 | & winner of the [Best Partner Award](https://frappe.io/partners/india/greycube-technologies) 38 | 39 |
40 | greycube.in
41 | 42 | sales@greycube.in
43 | LinkedIn
44 | Blogs
45 | -------------------------------------------------------------------------------- /erpnext_backup/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | __version__ = '0.0.1' 5 | 6 | -------------------------------------------------------------------------------- /erpnext_backup/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashish-greycube/erpnext_backup/b05a4c3b1071ffabaeba5f20c5e52f6fefea4f18/erpnext_backup/config/__init__.py -------------------------------------------------------------------------------- /erpnext_backup/config/desktop.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from frappe import _ 4 | 5 | def get_data(): 6 | return [ 7 | { 8 | "module_name": "ERPNext Backup", 9 | "color": "#f7f76f", 10 | "icon": "octicon octicon-cloud-upload", 11 | "type": "module", 12 | "label": _("ERPNext Backup") 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /erpnext_backup/config/docs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration for docs 3 | """ 4 | 5 | # source_link = "https://github.com/[org_name]/erpnext_backup" 6 | # docs_base_url = "https://[org_name].github.io/erpnext_backup" 7 | # headline = "App that does everything" 8 | # sub_heading = "Yes, you got that right the first time, everything" 9 | 10 | def get_context(context): 11 | context.brand_html = "ERPNext Backup" 12 | -------------------------------------------------------------------------------- /erpnext_backup/erpnext_backup/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashish-greycube/erpnext_backup/b05a4c3b1071ffabaeba5f20c5e52f6fefea4f18/erpnext_backup/erpnext_backup/__init__.py -------------------------------------------------------------------------------- /erpnext_backup/erpnext_backup/doctype/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashish-greycube/erpnext_backup/b05a4c3b1071ffabaeba5f20c5e52f6fefea4f18/erpnext_backup/erpnext_backup/doctype/__init__.py -------------------------------------------------------------------------------- /erpnext_backup/erpnext_backup/doctype/backup_settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashish-greycube/erpnext_backup/b05a4c3b1071ffabaeba5f20c5e52f6fefea4f18/erpnext_backup/erpnext_backup/doctype/backup_settings/__init__.py -------------------------------------------------------------------------------- /erpnext_backup/erpnext_backup/doctype/backup_settings/backup_settings.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018, GreyCube Technologies and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on('Backup Settings', { 5 | refresh: function (frm) { 6 | 7 | }, 8 | onload_post_render: function () { 9 | cur_frm.fields_dict.manual_backup.$input.addClass("btn-primary"); 10 | }, 11 | validate_send_notifications_to: function () { 12 | if (!cur_frm.doc.send_notifications_to) { 13 | msgprint(__("Please specify") + ": " + 14 | __(frappe.meta.get_label(cur_frm.doctype, 15 | "send_notifications_to"))); 16 | return false; 17 | } 18 | return true; 19 | }, 20 | manual_backup: function (frm) { 21 | if (frm.doc.enable_backup) { 22 | frappe.msgprint(__("Performing Backup")); 23 | frappe.call({ 24 | method: "erpnext_backup.erpnext_backup.doctype.backup_settings.backup_settings.take_backup", 25 | freeze: false, 26 | callback: function (r) { 27 | frappe.msgprint(r) 28 | } 29 | }) 30 | } else { 31 | frappe.msgprint(__("Backup is not enabled")); 32 | } 33 | } 34 | }); -------------------------------------------------------------------------------- /erpnext_backup/erpnext_backup/doctype/backup_settings/backup_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "allow_copy": 0, 3 | "allow_guest_to_view": 0, 4 | "allow_import": 0, 5 | "allow_rename": 0, 6 | "beta": 0, 7 | "creation": "2018-08-16 15:07:02.141312", 8 | "custom": 0, 9 | "description": "Backup Settings for Rclone based backup", 10 | "docstatus": 0, 11 | "doctype": "DocType", 12 | "document_type": "Setup", 13 | "editable_grid": 1, 14 | "engine": "InnoDB", 15 | "fields": [ 16 | { 17 | "allow_bulk_edit": 0, 18 | "allow_on_submit": 0, 19 | "bold": 0, 20 | "collapsible": 0, 21 | "columns": 0, 22 | "fieldname": "enable_backup", 23 | "fieldtype": "Check", 24 | "hidden": 0, 25 | "ignore_user_permissions": 0, 26 | "ignore_xss_filter": 0, 27 | "in_filter": 0, 28 | "in_global_search": 0, 29 | "in_list_view": 0, 30 | "in_standard_filter": 0, 31 | "label": "Enable Backup", 32 | "length": 0, 33 | "no_copy": 0, 34 | "permlevel": 0, 35 | "precision": "", 36 | "print_hide": 0, 37 | "print_hide_if_no_value": 0, 38 | "read_only": 0, 39 | "remember_last_selected_value": 0, 40 | "report_hide": 0, 41 | "reqd": 0, 42 | "search_index": 0, 43 | "set_only_once": 0, 44 | "unique": 0 45 | }, 46 | { 47 | "allow_bulk_edit": 0, 48 | "allow_on_submit": 0, 49 | "bold": 0, 50 | "collapsible": 0, 51 | "columns": 0, 52 | "default": "Daily", 53 | "fieldname": "upload_frequency", 54 | "fieldtype": "Select", 55 | "hidden": 0, 56 | "ignore_user_permissions": 0, 57 | "ignore_xss_filter": 0, 58 | "in_filter": 0, 59 | "in_global_search": 0, 60 | "in_list_view": 0, 61 | "in_standard_filter": 0, 62 | "label": "Backup Frequency", 63 | "length": 0, 64 | "no_copy": 0, 65 | "options": "Hourly\nEvery 6 Hours\nEvery 12 Hours\nDaily\nWeekly", 66 | "permlevel": 0, 67 | "precision": "", 68 | "print_hide": 0, 69 | "print_hide_if_no_value": 0, 70 | "read_only": 0, 71 | "remember_last_selected_value": 0, 72 | "report_hide": 0, 73 | "reqd": 0, 74 | "search_index": 0, 75 | "set_only_once": 0, 76 | "unique": 0 77 | }, 78 | { 79 | "allow_bulk_edit": 0, 80 | "allow_on_submit": 0, 81 | "bold": 0, 82 | "collapsible": 0, 83 | "columns": 0, 84 | "fieldname": "older_than", 85 | "fieldtype": "Int", 86 | "hidden": 0, 87 | "ignore_user_permissions": 0, 88 | "ignore_xss_filter": 0, 89 | "in_filter": 0, 90 | "in_global_search": 0, 91 | "in_list_view": 0, 92 | "in_standard_filter": 0, 93 | "label": "Older Than (Hrs)", 94 | "length": 0, 95 | "no_copy": 0, 96 | "permlevel": 0, 97 | "precision": "", 98 | "print_hide": 0, 99 | "print_hide_if_no_value": 0, 100 | "read_only": 0, 101 | "remember_last_selected_value": 0, 102 | "report_hide": 0, 103 | "reqd": 0, 104 | "search_index": 0, 105 | "set_only_once": 0, 106 | "unique": 0 107 | }, 108 | { 109 | "allow_bulk_edit": 0, 110 | "allow_on_submit": 0, 111 | "bold": 0, 112 | "collapsible": 0, 113 | "columns": 0, 114 | "fieldname": "backup_limit", 115 | "fieldtype": "Int", 116 | "hidden": 0, 117 | "ignore_user_permissions": 0, 118 | "ignore_xss_filter": 0, 119 | "in_filter": 0, 120 | "in_global_search": 0, 121 | "in_list_view": 0, 122 | "in_standard_filter": 0, 123 | "label": "Backup Limit", 124 | "length": 0, 125 | "no_copy": 0, 126 | "permlevel": 0, 127 | "precision": "", 128 | "print_hide": 0, 129 | "print_hide_if_no_value": 0, 130 | "read_only": 0, 131 | "remember_last_selected_value": 0, 132 | "report_hide": 0, 133 | "reqd": 0, 134 | "search_index": 0, 135 | "set_only_once": 0, 136 | "unique": 0 137 | }, 138 | { 139 | "allow_bulk_edit": 0, 140 | "allow_on_submit": 0, 141 | "bold": 0, 142 | "collapsible": 0, 143 | "columns": 0, 144 | "fieldname": "enable_database", 145 | "fieldtype": "Check", 146 | "hidden": 0, 147 | "ignore_user_permissions": 0, 148 | "ignore_xss_filter": 0, 149 | "in_filter": 0, 150 | "in_global_search": 0, 151 | "in_list_view": 0, 152 | "in_standard_filter": 0, 153 | "label": "Enable Database", 154 | "length": 0, 155 | "no_copy": 0, 156 | "permlevel": 0, 157 | "precision": "", 158 | "print_hide": 0, 159 | "print_hide_if_no_value": 0, 160 | "read_only": 0, 161 | "remember_last_selected_value": 0, 162 | "report_hide": 0, 163 | "reqd": 0, 164 | "search_index": 0, 165 | "set_only_once": 0, 166 | "unique": 0 167 | }, 168 | { 169 | "allow_bulk_edit": 0, 170 | "allow_on_submit": 0, 171 | "bold": 0, 172 | "collapsible": 0, 173 | "columns": 0, 174 | "fieldname": "enable_public_files", 175 | "fieldtype": "Check", 176 | "hidden": 0, 177 | "ignore_user_permissions": 0, 178 | "ignore_xss_filter": 0, 179 | "in_filter": 0, 180 | "in_global_search": 0, 181 | "in_list_view": 0, 182 | "in_standard_filter": 0, 183 | "label": " Enable Public Files", 184 | "length": 0, 185 | "no_copy": 0, 186 | "permlevel": 0, 187 | "precision": "", 188 | "print_hide": 0, 189 | "print_hide_if_no_value": 0, 190 | "read_only": 0, 191 | "remember_last_selected_value": 0, 192 | "report_hide": 0, 193 | "reqd": 0, 194 | "search_index": 0, 195 | "set_only_once": 0, 196 | "unique": 0 197 | }, 198 | { 199 | "allow_bulk_edit": 0, 200 | "allow_on_submit": 0, 201 | "bold": 0, 202 | "collapsible": 0, 203 | "columns": 0, 204 | "fieldname": "enable_private_files", 205 | "fieldtype": "Check", 206 | "hidden": 0, 207 | "ignore_user_permissions": 0, 208 | "ignore_xss_filter": 0, 209 | "in_filter": 0, 210 | "in_global_search": 0, 211 | "in_list_view": 0, 212 | "in_standard_filter": 0, 213 | "label": "Enable Private Files", 214 | "length": 0, 215 | "no_copy": 0, 216 | "permlevel": 0, 217 | "precision": "", 218 | "print_hide": 0, 219 | "print_hide_if_no_value": 0, 220 | "read_only": 0, 221 | "remember_last_selected_value": 0, 222 | "report_hide": 0, 223 | "reqd": 0, 224 | "search_index": 0, 225 | "set_only_once": 0, 226 | "unique": 0 227 | }, 228 | { 229 | "allow_bulk_edit": 0, 230 | "allow_on_submit": 0, 231 | "bold": 0, 232 | "collapsible": 0, 233 | "columns": 0, 234 | "fieldname": "column_break_7", 235 | "fieldtype": "Column Break", 236 | "hidden": 0, 237 | "ignore_user_permissions": 0, 238 | "ignore_xss_filter": 0, 239 | "in_filter": 0, 240 | "in_global_search": 0, 241 | "in_list_view": 0, 242 | "in_standard_filter": 0, 243 | "length": 0, 244 | "no_copy": 0, 245 | "permlevel": 0, 246 | "precision": "", 247 | "print_hide": 0, 248 | "print_hide_if_no_value": 0, 249 | "read_only": 0, 250 | "remember_last_selected_value": 0, 251 | "report_hide": 0, 252 | "reqd": 0, 253 | "search_index": 0, 254 | "set_only_once": 0, 255 | "unique": 0 256 | }, 257 | { 258 | "allow_bulk_edit": 0, 259 | "allow_on_submit": 0, 260 | "bold": 0, 261 | "collapsible": 0, 262 | "columns": 0, 263 | "fieldname": "cloud_sync", 264 | "fieldtype": "Check", 265 | "hidden": 0, 266 | "ignore_user_permissions": 0, 267 | "ignore_xss_filter": 0, 268 | "in_filter": 0, 269 | "in_global_search": 0, 270 | "in_list_view": 0, 271 | "in_standard_filter": 0, 272 | "label": "RClone Sync", 273 | "length": 0, 274 | "no_copy": 0, 275 | "permlevel": 0, 276 | "precision": "", 277 | "print_hide": 0, 278 | "print_hide_if_no_value": 0, 279 | "read_only": 0, 280 | "remember_last_selected_value": 0, 281 | "report_hide": 0, 282 | "reqd": 0, 283 | "search_index": 0, 284 | "set_only_once": 0, 285 | "unique": 0 286 | }, 287 | { 288 | "allow_bulk_edit": 0, 289 | "allow_on_submit": 0, 290 | "bold": 0, 291 | "collapsible": 0, 292 | "columns": 0, 293 | "depends_on": "eval:doc.cloud_sync==1", 294 | "fieldname": "rclone_remote_name", 295 | "fieldtype": "Data", 296 | "hidden": 0, 297 | "ignore_user_permissions": 0, 298 | "ignore_xss_filter": 0, 299 | "in_filter": 0, 300 | "in_global_search": 0, 301 | "in_list_view": 0, 302 | "in_standard_filter": 0, 303 | "label": "RClone Remote Name", 304 | "length": 0, 305 | "no_copy": 0, 306 | "permlevel": 0, 307 | "precision": "", 308 | "print_hide": 0, 309 | "print_hide_if_no_value": 0, 310 | "read_only": 0, 311 | "remember_last_selected_value": 0, 312 | "report_hide": 0, 313 | "reqd": 0, 314 | "search_index": 0, 315 | "set_only_once": 0, 316 | "unique": 0 317 | }, 318 | { 319 | "allow_bulk_edit": 0, 320 | "allow_on_submit": 0, 321 | "bold": 0, 322 | "collapsible": 0, 323 | "columns": 0, 324 | "depends_on": "eval:doc.cloud_sync==1", 325 | "fieldname": "rclone_remote_directory_path", 326 | "fieldtype": "Data", 327 | "hidden": 0, 328 | "ignore_user_permissions": 0, 329 | "ignore_xss_filter": 0, 330 | "in_filter": 0, 331 | "in_global_search": 0, 332 | "in_list_view": 0, 333 | "in_standard_filter": 0, 334 | "label": "RClone Remote Directory Path", 335 | "length": 0, 336 | "no_copy": 0, 337 | "permlevel": 0, 338 | "precision": "", 339 | "print_hide": 0, 340 | "print_hide_if_no_value": 0, 341 | "read_only": 0, 342 | "remember_last_selected_value": 0, 343 | "report_hide": 0, 344 | "reqd": 0, 345 | "search_index": 0, 346 | "set_only_once": 0, 347 | "unique": 0 348 | }, 349 | { 350 | "allow_bulk_edit": 0, 351 | "allow_on_submit": 0, 352 | "bold": 0, 353 | "collapsible": 0, 354 | "columns": 0, 355 | "fieldname": "section_break_11", 356 | "fieldtype": "Section Break", 357 | "hidden": 0, 358 | "ignore_user_permissions": 0, 359 | "ignore_xss_filter": 0, 360 | "in_filter": 0, 361 | "in_global_search": 0, 362 | "in_list_view": 0, 363 | "in_standard_filter": 0, 364 | "length": 0, 365 | "no_copy": 0, 366 | "permlevel": 0, 367 | "precision": "", 368 | "print_hide": 0, 369 | "print_hide_if_no_value": 0, 370 | "read_only": 0, 371 | "remember_last_selected_value": 0, 372 | "report_hide": 0, 373 | "reqd": 0, 374 | "search_index": 0, 375 | "set_only_once": 0, 376 | "unique": 0 377 | }, 378 | { 379 | "allow_bulk_edit": 0, 380 | "allow_on_submit": 0, 381 | "bold": 0, 382 | "collapsible": 0, 383 | "columns": 0, 384 | "fieldname": "send_notifications_to", 385 | "fieldtype": "Data", 386 | "hidden": 0, 387 | "ignore_user_permissions": 0, 388 | "ignore_xss_filter": 0, 389 | "in_filter": 0, 390 | "in_global_search": 0, 391 | "in_list_view": 0, 392 | "in_standard_filter": 0, 393 | "label": "Send Notifications To", 394 | "length": 0, 395 | "no_copy": 0, 396 | "permlevel": 0, 397 | "precision": "", 398 | "print_hide": 0, 399 | "print_hide_if_no_value": 0, 400 | "read_only": 0, 401 | "remember_last_selected_value": 0, 402 | "report_hide": 0, 403 | "reqd": 0, 404 | "search_index": 0, 405 | "set_only_once": 0, 406 | "unique": 0 407 | }, 408 | { 409 | "allow_bulk_edit": 0, 410 | "allow_on_submit": 0, 411 | "bold": 0, 412 | "collapsible": 0, 413 | "columns": 0, 414 | "fieldname": "manual_backup", 415 | "fieldtype": "Button", 416 | "hidden": 0, 417 | "ignore_user_permissions": 0, 418 | "ignore_xss_filter": 0, 419 | "in_filter": 0, 420 | "in_global_search": 0, 421 | "in_list_view": 0, 422 | "in_standard_filter": 0, 423 | "label": "Manual Backup", 424 | "length": 0, 425 | "no_copy": 0, 426 | "permlevel": 0, 427 | "precision": "", 428 | "print_hide": 0, 429 | "print_hide_if_no_value": 0, 430 | "read_only": 0, 431 | "remember_last_selected_value": 0, 432 | "report_hide": 0, 433 | "reqd": 0, 434 | "search_index": 0, 435 | "set_only_once": 0, 436 | "unique": 0 437 | }, 438 | { 439 | "allow_bulk_edit": 0, 440 | "allow_on_submit": 0, 441 | "bold": 0, 442 | "collapsible": 0, 443 | "columns": 0, 444 | "fieldname": "last_backup_date", 445 | "fieldtype": "Datetime", 446 | "hidden": 0, 447 | "ignore_user_permissions": 0, 448 | "ignore_xss_filter": 0, 449 | "in_filter": 0, 450 | "in_global_search": 0, 451 | "in_list_view": 0, 452 | "in_standard_filter": 0, 453 | "label": "Last Backup Date", 454 | "length": 0, 455 | "no_copy": 0, 456 | "permlevel": 0, 457 | "precision": "", 458 | "print_hide": 0, 459 | "print_hide_if_no_value": 0, 460 | "read_only": 1, 461 | "remember_last_selected_value": 0, 462 | "report_hide": 0, 463 | "reqd": 0, 464 | "search_index": 0, 465 | "set_only_once": 0, 466 | "unique": 0 467 | } 468 | ], 469 | "has_web_view": 0, 470 | "hide_heading": 0, 471 | "hide_toolbar": 0, 472 | "idx": 0, 473 | "image_view": 0, 474 | "in_create": 0, 475 | "is_submittable": 0, 476 | "issingle": 1, 477 | "istable": 0, 478 | "max_attachments": 0, 479 | "modified": "2018-10-20 19:44:16.385857", 480 | "modified_by": "Administrator", 481 | "module": "ERPNext Backup", 482 | "name": "Backup Settings", 483 | "name_case": "", 484 | "owner": "Administrator", 485 | "permissions": [ 486 | { 487 | "amend": 0, 488 | "apply_user_permissions": 0, 489 | "cancel": 0, 490 | "create": 1, 491 | "delete": 1, 492 | "email": 1, 493 | "export": 0, 494 | "if_owner": 0, 495 | "import": 0, 496 | "permlevel": 0, 497 | "print": 1, 498 | "read": 1, 499 | "report": 0, 500 | "role": "System Manager", 501 | "set_user_permissions": 0, 502 | "share": 1, 503 | "submit": 0, 504 | "write": 1 505 | } 506 | ], 507 | "quick_entry": 1, 508 | "read_only": 0, 509 | "read_only_onload": 0, 510 | "show_name_in_global_search": 0, 511 | "sort_field": "modified", 512 | "sort_order": "DESC", 513 | "track_changes": 1, 514 | "track_seen": 0 515 | } -------------------------------------------------------------------------------- /erpnext_backup/erpnext_backup/doctype/backup_settings/backup_settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2018, GreyCube Technologies and contributors 3 | # For license information, please see license.txt 4 | 5 | from __future__ import unicode_literals 6 | import frappe 7 | from frappe.model.document import Document 8 | from frappe.utils.background_jobs import enqueue 9 | from frappe.utils import cint, split_emails, get_site_base_path, cstr, today,get_backups_path,get_datetime 10 | from datetime import datetime, timedelta 11 | from frappe.utils import get_site_path, cint, get_url 12 | 13 | import os 14 | from frappe import _ 15 | from frappe.utils.file_manager import save_file_on_filesystem 16 | from frappe.utils.change_log import get_versions 17 | #Global constants 18 | verbose = 0 19 | ignore_list = [".DS_Store"] 20 | 21 | class BackupSettings(Document): 22 | pass 23 | 24 | def take_backups_hourly(): 25 | take_backups_if("Hourly") 26 | 27 | def take_backups_daily(): 28 | take_backups_if("Daily") 29 | 30 | def take_backups_weekly(): 31 | take_backups_if("Weekly") 32 | 33 | def take_backups_if(freq): 34 | if cint(frappe.db.get_value("Backup Settings", None, "enable_backup")): 35 | upload_frequency = frappe.db.get_value("Backup Settings", None, "upload_frequency") 36 | if upload_frequency == freq: 37 | take_backup() 38 | elif freq == "Hourly" and upload_frequency in ["Every 6 Hours","Every 12 Hours"]: 39 | last_backup_date = frappe.db.get_value('Backup Settings', None, 'last_backup_date') 40 | upload_interval = 12 41 | if upload_frequency == "Every 6 Hours": 42 | upload_interval = 6 43 | elif upload_frequency == "Every 12 Hours": 44 | upload_interval = 12 45 | 46 | if datetime.now() - get_datetime(last_backup_date) >= timedelta(hours = upload_interval): 47 | take_backup() 48 | 49 | 50 | @frappe.whitelist() 51 | def take_backup(): 52 | # "Enqueue longjob for taking backup to dropbox" 53 | #enqueue("erpnext_backup.erpnext_backup.doctype.backup_settings.backup_settings.take_backup_to_service", queue='short', timeout=1500) 54 | take_backup_to_service() 55 | return 56 | 57 | 58 | def take_backup_to_service(): 59 | 60 | did_not_upload, error_log = [], [] 61 | try: 62 | 63 | # create a file to note the app and its versions 64 | content=frappe.as_json(frappe.utils.change_log.get_versions()) 65 | save_file_on_filesystem('app_name_versions.txt',content , content_type='utf-8', is_private=0) 66 | 67 | did_not_upload, error_log = backup_to_service() 68 | if did_not_upload: raise Exception 69 | 70 | frappe.db.begin() 71 | frappe.db.set_value('Backup Settings', 'Backup Settings', 'last_backup_date', datetime.now()) 72 | frappe.db.commit() 73 | 74 | #send_email(True, "Backup") 75 | except Exception: 76 | file_and_error = [" - ".join(f) for f in zip(did_not_upload, error_log)] 77 | error_message = ("\n".join(file_and_error) + "\n" + frappe.get_traceback()) 78 | # frappe.errprint(error_message) 79 | send_email(False, "Backup", error_message) 80 | 81 | 82 | 83 | 84 | 85 | def send_email(success, service_name, error_status=None): 86 | if success: 87 | subject = "Backup Upload Successful" 88 | message ="""

Backup Uploaded Successfully

Hi there, this is just to inform you 89 | that your backup was successfully uploaded to your %s account.

90 | """ % service_name 91 | 92 | else: 93 | subject = "[Warning] Backup Upload Failed" 94 | message ="""

Backup Upload Failed

Oops, your automated backup to %s 95 | failed.

96 |

Error message: %s

97 |

Please contact your system manager for more information.

98 | """ % (service_name, error_status) 99 | 100 | if not frappe.db: 101 | frappe.connect() 102 | 103 | 104 | recipients = split_emails(frappe.db.get_value("Backup Settings", None, "send_notifications_to")) 105 | frappe.sendmail(recipients=recipients, subject=subject, message=message) 106 | 107 | def get_scheduled_backup_limit(): 108 | backup_limit = frappe.db.get_singles_value('Backup Settings', 'backup_limit') 109 | return cint(backup_limit) 110 | 111 | def cleanup_old_backups(site_path, files, limit,endswith): 112 | backup_paths = [] 113 | for f in files: 114 | if f.endswith(endswith): 115 | _path = os.path.abspath(os.path.join(site_path, f)) 116 | backup_paths.append(_path) 117 | 118 | backup_paths = sorted(backup_paths, key=os.path.getctime) 119 | files_to_delete = len(backup_paths) - limit 120 | 121 | for idx in range(0, files_to_delete): 122 | f = os.path.basename(backup_paths[idx]) 123 | files.remove(f) 124 | os.remove(backup_paths[idx]) 125 | 126 | 127 | @frappe.whitelist() 128 | def backup_to_service(): 129 | from frappe.utils.backups import new_backup 130 | from frappe.utils import get_files_path 131 | 132 | 133 | #delete old 134 | path = get_site_path('private', 'backups') 135 | files = [x for x in os.listdir(path) if os.path.isfile(os.path.join(path, x))] 136 | backup_limit = get_scheduled_backup_limit() 137 | endswith='sql.gz' 138 | if len(files) > backup_limit: 139 | cleanup_old_backups(path, files, backup_limit,endswith) 140 | 141 | endswith='files.tar' 142 | if len(files) > backup_limit: 143 | cleanup_old_backups(path, files, backup_limit,endswith) 144 | 145 | endswith='private-files.tar' 146 | if len(files) > backup_limit: 147 | cleanup_old_backups(path, files, backup_limit,endswith) 148 | 149 | 150 | #delete old 151 | 152 | # upload files to files folder 153 | did_not_upload = [] 154 | error_log = [] 155 | 156 | if not frappe.db: 157 | frappe.connect() 158 | 159 | older_than = cint(frappe.db.get_value('Backup Settings', None, 'older_than')) 160 | cloud_sync = cint(frappe.db.get_value('Backup Settings', None, 'cloud_sync')) 161 | 162 | site = frappe.db.get_value('Global Defaults', None, 'default_company') 163 | 164 | if cint(frappe.db.get_value("Backup Settings", None, "enable_database")): 165 | # upload database 166 | # backup = new_backup(older_than,ignore_files=True) 167 | backup = new_backup(ignore_files=False, backup_path_db=None, 168 | backup_path_files=None, backup_path_private_files=None, force=True) 169 | db_filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_db)) 170 | files_filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_files)) 171 | private_files = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_private_files)) 172 | folder = os.path.basename(db_filename)[:15] + '/' 173 | 174 | 175 | # filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_db)) 176 | if cloud_sync: 177 | sync_folder(site,older_than,db_filename, "database",did_not_upload,error_log) 178 | 179 | BASE_DIR = os.path.join( get_backups_path(), '../file_backups' ) 180 | # print(get_backups_path()) 181 | # print BASE_DIR 182 | 183 | 184 | if cint(frappe.db.get_value("Backup Settings", None, "enable_public_files")): 185 | # Backup_DIR = os.path.join(BASE_DIR, "files") 186 | # compress_files(get_files_path(), Backup_DIR) 187 | if cloud_sync: 188 | sync_folder(site,older_than,files_filename, "public-files",did_not_upload,error_log) 189 | 190 | 191 | if cint(frappe.db.get_value("Backup Settings", None, "enable_private_files")): 192 | # Backup_DIR = os.path.join(BASE_DIR, "private/files") 193 | # compress_files(get_files_path(is_private=1), Backup_DIR) 194 | if cloud_sync: 195 | sync_folder(site,older_than,private_files, "private-files",did_not_upload,error_log) 196 | 197 | frappe.db.close() 198 | # frappe.connect() 199 | return did_not_upload, list(set(error_log)) 200 | 201 | def compress_files(file_DIR, Backup_DIR): 202 | if not os.path.exists(file_DIR): 203 | return 204 | 205 | from shutil import make_archive 206 | archivename = datetime.today().strftime("%d%m%Y_%H%M%S")+'_files' 207 | archivepath = os.path.join(Backup_DIR,archivename) 208 | make_archive(archivepath,'zip',file_DIR) 209 | 210 | 211 | def sync_folder(site,older_than,sourcepath, destfolder,did_not_upload,error_log): 212 | # destpath = "gdrive:" + destfolder + " --drive-use-trash" 213 | rclone_remote_directory=frappe.db.get_value('Backup Settings', None, 'rclone_remote_directory_path') 214 | from frappe.utils import get_bench_path 215 | sourcepath=get_bench_path()+"/sites"+sourcepath.replace("./", "/") 216 | # final_dest = rclone_remote_directory+"/"+str(site) + "/" + destfolder 217 | 218 | final_dest = rclone_remote_directory+"/"+str(site) 219 | final_dest = final_dest.replace(" ", "_") 220 | rclone_remote_name=frappe.db.get_value('Backup Settings', None, 'rclone_remote_name') 221 | # rclone_remote_directory=frappe.db.get_value('Backup Settings', None, 'rclone_remote_directory_path') 222 | 223 | # destpath = rclone_remote_name+":"+rclone_remote_directory+'/'+final_dest 224 | destpath = rclone_remote_name+":"+final_dest 225 | 226 | BASE_DIR = os.path.join( get_backups_path(), '../file_backups' ) 227 | Backup_DIR = os.path.join(BASE_DIR, "private/files") 228 | 229 | # print older_than 230 | # delete_temp_backups(older_than,sourcepath) 231 | sourcepath = get_bench_path()+"/sites"+get_backups_path().replace("./", "/") 232 | cmd_string = "rclone sync " + sourcepath + " " + destpath 233 | 234 | # frappe.errprint(cmd_string) 235 | try: 236 | err, out = frappe.utils.execute_in_shell(cmd_string) 237 | if err: raise Exception 238 | except Exception: 239 | did_not_upload = True 240 | error_log.append(Exception) 241 | 242 | 243 | 244 | def delete_temp_backups(older_than, path): 245 | """ 246 | Cleans up the backup_link_path directory by deleting files older than x hours 247 | """ 248 | file_list = os.listdir(path) 249 | for this_file in file_list: 250 | this_file_path = os.path.join(path, this_file) 251 | if is_file_old(this_file_path, older_than): 252 | os.remove(this_file_path) 253 | 254 | 255 | 256 | def is_file_old(db_file_name, older_than=24): 257 | """ 258 | Checks if file exists and is older than specified hours 259 | Returns -> 260 | True: file does not exist or file is old 261 | False: file is new 262 | """ 263 | if os.path.isfile(db_file_name): 264 | from datetime import timedelta 265 | #Get timestamp of the file 266 | file_datetime = datetime.fromtimestamp\ 267 | (os.stat(db_file_name).st_ctime) 268 | if datetime.today() - file_datetime >= timedelta(hours = older_than): 269 | if verbose: print("File is old") 270 | return True 271 | else: 272 | if verbose: print("File is recent") 273 | return False 274 | else: 275 | if verbose: print("File does not exist") 276 | return True 277 | 278 | 279 | if __name__=="__main__": 280 | backup_to_service() -------------------------------------------------------------------------------- /erpnext_backup/erpnext_backup/doctype/backup_settings/test_backup_settings.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // rename this file from _test_[name] to test_[name] to activate 3 | // and remove above this line 4 | 5 | QUnit.test("test: Backup Settings", function (assert) { 6 | let done = assert.async(); 7 | 8 | // number of asserts 9 | assert.expect(1); 10 | 11 | frappe.run_serially([ 12 | // insert a new Backup Settings 13 | () => frappe.tests.make('Backup Settings', [ 14 | // values to be set 15 | {key: 'value'} 16 | ]), 17 | () => { 18 | assert.equal(cur_frm.doc.key, 'value'); 19 | }, 20 | () => done() 21 | ]); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /erpnext_backup/erpnext_backup/doctype/backup_settings/test_backup_settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2018, GreyCube Technologies and Contributors 3 | # See license.txt 4 | from __future__ import unicode_literals 5 | 6 | import frappe 7 | import unittest 8 | 9 | class TestBackupSettings(unittest.TestCase): 10 | pass 11 | -------------------------------------------------------------------------------- /erpnext_backup/hooks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from . import __version__ as app_version 4 | 5 | app_name = "erpnext_backup" 6 | app_title = "ERPNext Backup" 7 | app_publisher = "GreyCube Technologies" 8 | app_description = "App for auto backup of ERPNext files/database to remote server using rclone" 9 | app_icon = "octicon octicon-cloud-upload" 10 | app_color = "#f7f76f" 11 | app_email = "admin@greycube.in" 12 | app_license = "MIT" 13 | 14 | # Includes in 15 | # ------------------ 16 | 17 | # include js, css files in header of desk.html 18 | # app_include_css = "/assets/erpnext_backup/css/erpnext_backup.css" 19 | # app_include_js = "/assets/erpnext_backup/js/erpnext_backup.js" 20 | 21 | # include js, css files in header of web template 22 | # web_include_css = "/assets/erpnext_backup/css/erpnext_backup.css" 23 | # web_include_js = "/assets/erpnext_backup/js/erpnext_backup.js" 24 | 25 | # include js in page 26 | # page_js = {"page" : "public/js/file.js"} 27 | 28 | # include js in doctype views 29 | # doctype_js = {"doctype" : "public/js/doctype.js"} 30 | # doctype_list_js = {"doctype" : "public/js/doctype_list.js"} 31 | # doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"} 32 | # doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"} 33 | 34 | # Home Pages 35 | # ---------- 36 | 37 | # application home page (will override Website Settings) 38 | # home_page = "login" 39 | 40 | # website user home page (by Role) 41 | # role_home_page = { 42 | # "Role": "home_page" 43 | # } 44 | 45 | # Website user home page (by function) 46 | # get_website_user_home_page = "erpnext_backup.utils.get_home_page" 47 | 48 | # Generators 49 | # ---------- 50 | 51 | # automatically create page for each record of this doctype 52 | # website_generators = ["Web Page"] 53 | 54 | # Installation 55 | # ------------ 56 | 57 | # before_install = "erpnext_backup.install.before_install" 58 | # after_install = "erpnext_backup.install.after_install" 59 | 60 | # Desk Notifications 61 | # ------------------ 62 | # See frappe.core.notifications.get_notification_config 63 | 64 | # notification_config = "erpnext_backup.notifications.get_notification_config" 65 | 66 | # Permissions 67 | # ----------- 68 | # Permissions evaluated in scripted ways 69 | 70 | # permission_query_conditions = { 71 | # "Event": "frappe.desk.doctype.event.event.get_permission_query_conditions", 72 | # } 73 | # 74 | # has_permission = { 75 | # "Event": "frappe.desk.doctype.event.event.has_permission", 76 | # } 77 | 78 | # Document Events 79 | # --------------- 80 | # Hook on document methods and events 81 | 82 | # doc_events = { 83 | # "*": { 84 | # "on_update": "method", 85 | # "on_cancel": "method", 86 | # "on_trash": "method" 87 | # } 88 | # } 89 | 90 | # Scheduled Tasks 91 | # --------------- 92 | 93 | scheduler_events = { 94 | # "all": [ 95 | # "" 96 | # ], 97 | "daily": [ 98 | "erpnext_backup.erpnext_backup.doctype.backup_settings.backup_settings.take_backups_daily" 99 | ], 100 | "hourly": [ 101 | "erpnext_backup.erpnext_backup.doctype.backup_settings.backup_settings.take_backups_hourly" 102 | ], 103 | "weekly": [ 104 | "erpnext_backup.erpnext_backup.doctype.backup_settings.backup_settings.take_backups_weekly" 105 | ] 106 | # "monthly": [ 107 | # "" 108 | # ] 109 | } 110 | 111 | # Testing 112 | # ------- 113 | 114 | # before_tests = "erpnext_backup.install.before_tests" 115 | 116 | # Overriding Whitelisted Methods 117 | # ------------------------------ 118 | # 119 | # override_whitelisted_methods = { 120 | # "frappe.desk.doctype.event.event.get_events": "erpnext_backup.event.get_events" 121 | # } 122 | 123 | -------------------------------------------------------------------------------- /erpnext_backup/modules.txt: -------------------------------------------------------------------------------- 1 | ERPNext Backup -------------------------------------------------------------------------------- /erpnext_backup/patches.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashish-greycube/erpnext_backup/b05a4c3b1071ffabaeba5f20c5e52f6fefea4f18/erpnext_backup/patches.txt -------------------------------------------------------------------------------- /erpnext_backup/templates/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashish-greycube/erpnext_backup/b05a4c3b1071ffabaeba5f20c5e52f6fefea4f18/erpnext_backup/templates/__init__.py -------------------------------------------------------------------------------- /erpnext_backup/templates/pages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashish-greycube/erpnext_backup/b05a4c3b1071ffabaeba5f20c5e52f6fefea4f18/erpnext_backup/templates/pages/__init__.py -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | License: MIT -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | frappe -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from setuptools import setup, find_packages 3 | import re, ast 4 | 5 | with open('requirements.txt') as f: 6 | install_requires = f.read().strip().split('\n') 7 | 8 | # get version from __version__ variable in erpnext_backup/__init__.py 9 | _version_re = re.compile(r'__version__\s+=\s+(.*)') 10 | 11 | with open('erpnext_backup/__init__.py', 'rb') as f: 12 | version = str(ast.literal_eval(_version_re.search( 13 | f.read().decode('utf-8')).group(1))) 14 | 15 | setup( 16 | name='erpnext_backup', 17 | version=version, 18 | description='App for auto backup of ERPNext files/database to remote server using rclone', 19 | author='GreyCube Technologies', 20 | author_email='admin@greycube.in', 21 | packages=find_packages(), 22 | zip_safe=False, 23 | include_package_data=True, 24 | install_requires=install_requires 25 | ) 26 | --------------------------------------------------------------------------------