├── .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 |
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 |
--------------------------------------------------------------------------------