Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 
 
 
 
 

381 rader
12 KiB

  1. # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  2. # MIT License. See license.txt
  3. """This module handles the On Demand Backup utility"""
  4. from __future__ import print_function, unicode_literals
  5. import os
  6. import json
  7. from calendar import timegm
  8. from datetime import datetime
  9. from glob import glob
  10. import frappe
  11. from frappe import _, conf
  12. from frappe.utils import cstr, get_url, now_datetime
  13. # backup variable for backwards compatibility
  14. verbose = False
  15. _verbose = verbose
  16. class BackupGenerator:
  17. """
  18. This class contains methods to perform On Demand Backup
  19. To initialize, specify (db_name, user, password, db_file_name=None, db_host="localhost")
  20. If specifying db_file_name, also append ".sql.gz"
  21. """
  22. def __init__(self, db_name, user, password, backup_path_db=None, backup_path_files=None,
  23. backup_path_private_files=None, db_host="localhost", db_port=None, verbose=False,
  24. db_type='mariadb', backup_path_conf=None):
  25. global _verbose
  26. self.db_host = db_host
  27. self.db_port = db_port
  28. self.db_name = db_name
  29. self.db_type = db_type
  30. self.user = user
  31. self.password = password
  32. self.backup_path_conf = backup_path_conf
  33. self.backup_path_db = backup_path_db
  34. self.backup_path_files = backup_path_files
  35. self.backup_path_private_files = backup_path_private_files
  36. if not self.db_type:
  37. self.db_type = 'mariadb'
  38. if not self.db_port and self.db_type == 'mariadb':
  39. self.db_port = 3306
  40. elif not self.db_port and self.db_type == 'postgres':
  41. self.db_port = 5432
  42. site = frappe.local.site or frappe.generate_hash(length=8)
  43. self.site_slug = site.replace('.', '_')
  44. self.verbose = verbose
  45. self.setup_backup_directory()
  46. _verbose = verbose
  47. def setup_backup_directory(self):
  48. specified = self.backup_path_db or self.backup_path_files or self.backup_path_private_files
  49. if not specified:
  50. backups_folder = get_backup_path()
  51. if not os.path.exists(backups_folder):
  52. os.makedirs(backups_folder)
  53. else:
  54. for file_path in [self.backup_path_files, self.backup_path_db, self.backup_path_private_files]:
  55. dir = os.path.dirname(file_path)
  56. os.makedirs(dir, exist_ok=True)
  57. def get_backup(self, older_than=24, ignore_files=False, force=False):
  58. """
  59. Takes a new dump if existing file is old
  60. and sends the link to the file as email
  61. """
  62. #Check if file exists and is less than a day old
  63. #If not Take Dump
  64. if not force:
  65. last_db, last_file, last_private_file, site_config_backup_path = self.get_recent_backup(older_than)
  66. else:
  67. last_db, last_file, last_private_file, site_config_backup_path = False, False, False, False
  68. self.todays_date = now_datetime().strftime('%Y%m%d_%H%M%S')
  69. if not (self.backup_path_files and self.backup_path_db and self.backup_path_private_files):
  70. self.set_backup_file_name()
  71. if not (last_db and last_file and last_private_file and site_config_backup_path):
  72. self.take_dump()
  73. self.copy_site_config()
  74. if not ignore_files:
  75. self.zip_files()
  76. else:
  77. self.backup_path_files = last_file
  78. self.backup_path_db = last_db
  79. self.backup_path_private_files = last_private_file
  80. self.site_config_backup_path = site_config_backup_path
  81. def set_backup_file_name(self):
  82. #Generate a random name using today's date and a 8 digit random number
  83. for_conf = self.todays_date + "-" + self.site_slug + "-site_config_backup.json"
  84. for_db = self.todays_date + "-" + self.site_slug + "-database.sql.gz"
  85. for_public_files = self.todays_date + "-" + self.site_slug + "-files.tar"
  86. for_private_files = self.todays_date + "-" + self.site_slug + "-private-files.tar"
  87. backup_path = get_backup_path()
  88. if not self.backup_path_conf:
  89. self.backup_path_conf = os.path.join(backup_path, for_conf)
  90. if not self.backup_path_db:
  91. self.backup_path_db = os.path.join(backup_path, for_db)
  92. if not self.backup_path_files:
  93. self.backup_path_files = os.path.join(backup_path, for_public_files)
  94. if not self.backup_path_private_files:
  95. self.backup_path_private_files = os.path.join(backup_path, for_private_files)
  96. def get_recent_backup(self, older_than):
  97. backup_path = get_backup_path()
  98. file_type_slugs = {
  99. "database": "*-{}-database.sql.gz",
  100. "public": "*-{}-files.tar",
  101. "private": "*-{}-private-files.tar",
  102. "config": "*-{}-site_config_backup.json",
  103. }
  104. def backup_time(file_path):
  105. file_name = file_path.split(os.sep)[-1]
  106. file_timestamp = file_name.split("-")[0]
  107. return timegm(datetime.strptime(file_timestamp, "%Y%m%d_%H%M%S").utctimetuple())
  108. def get_latest(file_pattern):
  109. file_pattern = os.path.join(backup_path, file_pattern.format(self.site_slug))
  110. file_list = glob(file_pattern)
  111. if file_list:
  112. return max(file_list, key=backup_time)
  113. def old_enough(file_path):
  114. if file_path:
  115. if not os.path.isfile(file_path) or is_file_old(file_path, older_than):
  116. return None
  117. return file_path
  118. latest_backups = {
  119. file_type: get_latest(pattern)
  120. for file_type, pattern in file_type_slugs.items()
  121. }
  122. recent_backups = {
  123. file_type: old_enough(file_name) for file_type, file_name in latest_backups.items()
  124. }
  125. return (
  126. recent_backups.get("database"),
  127. recent_backups.get("public"),
  128. recent_backups.get("private"),
  129. recent_backups.get("config"),
  130. )
  131. def zip_files(self):
  132. for folder in ("public", "private"):
  133. files_path = frappe.get_site_path(folder, "files")
  134. backup_path = self.backup_path_files if folder=="public" else self.backup_path_private_files
  135. cmd_string = """tar -cf %s %s""" % (backup_path, files_path)
  136. err, out = frappe.utils.execute_in_shell(cmd_string)
  137. if self.verbose:
  138. print('Backed up files', os.path.abspath(backup_path))
  139. def copy_site_config(self):
  140. site_config_backup_path = self.backup_path_conf
  141. site_config_path = os.path.join(frappe.get_site_path(), "site_config.json")
  142. with open(site_config_backup_path, "w") as n, open(site_config_path) as c:
  143. n.write(c.read())
  144. def take_dump(self):
  145. import frappe.utils
  146. # escape reserved characters
  147. args = dict([item[0], frappe.utils.esc(str(item[1]), '$ ')]
  148. for item in self.__dict__.copy().items())
  149. cmd_string = """mysqldump --single-transaction --quick --lock-tables=false -u %(user)s -p%(password)s %(db_name)s -h %(db_host)s -P %(db_port)s | gzip > %(backup_path_db)s """ % args
  150. if self.db_type == 'postgres':
  151. cmd_string = "pg_dump postgres://{user}:{password}@{db_host}:{db_port}/{db_name} | gzip > {backup_path_db}".format(
  152. user=args.get('user'),
  153. password=args.get('password'),
  154. db_host=args.get('db_host'),
  155. db_port=args.get('db_port'),
  156. db_name=args.get('db_name'),
  157. backup_path_db=args.get('backup_path_db')
  158. )
  159. err, out = frappe.utils.execute_in_shell(cmd_string)
  160. def send_email(self):
  161. """
  162. Sends the link to backup file located at erpnext/backups
  163. """
  164. from frappe.email import get_system_managers
  165. recipient_list = get_system_managers()
  166. db_backup_url = get_url(os.path.join('backups', os.path.basename(self.backup_path_db)))
  167. files_backup_url = get_url(os.path.join('backups', os.path.basename(self.backup_path_files)))
  168. msg = """Hello,
  169. Your backups are ready to be downloaded.
  170. 1. [Click here to download the database backup](%(db_backup_url)s)
  171. 2. [Click here to download the files backup](%(files_backup_url)s)
  172. This link will be valid for 24 hours. A new backup will be available for
  173. download only after 24 hours.""" % {
  174. "db_backup_url": db_backup_url,
  175. "files_backup_url": files_backup_url
  176. }
  177. datetime_str = datetime.fromtimestamp(os.stat(self.backup_path_db).st_ctime)
  178. subject = datetime_str.strftime("%d/%m/%Y %H:%M:%S") + """ - Backup ready to be downloaded"""
  179. frappe.sendmail(recipients=recipient_list, msg=msg, subject=subject)
  180. return recipient_list
  181. @frappe.whitelist()
  182. def get_backup():
  183. """
  184. This function is executed when the user clicks on
  185. Toos > Download Backup
  186. """
  187. delete_temp_backups()
  188. odb = BackupGenerator(frappe.conf.db_name, frappe.conf.db_name,\
  189. frappe.conf.db_password, db_host = frappe.db.host,\
  190. db_type=frappe.conf.db_type, db_port=frappe.conf.db_port)
  191. odb.get_backup()
  192. recipient_list = odb.send_email()
  193. frappe.msgprint(_("Download link for your backup will be emailed on the following email address: {0}").format(', '.join(recipient_list)))
  194. @frappe.whitelist()
  195. def fetch_latest_backups():
  196. """Fetches paths of the latest backup taken in the last 30 days
  197. Only for: System Managers
  198. Returns:
  199. dict: relative Backup Paths
  200. """
  201. frappe.only_for("System Manager")
  202. odb = BackupGenerator(
  203. frappe.conf.db_name,
  204. frappe.conf.db_name,
  205. frappe.conf.db_password,
  206. db_host=frappe.db.host,
  207. db_type=frappe.conf.db_type,
  208. db_port=frappe.conf.db_port,
  209. )
  210. database, public, private, config = odb.get_recent_backup(older_than=24 * 30)
  211. return {
  212. "database": database,
  213. "public": public,
  214. "private": private,
  215. "config": config
  216. }
  217. def scheduled_backup(older_than=6, ignore_files=False, backup_path_db=None, backup_path_files=None, backup_path_private_files=None, force=False, verbose=False):
  218. """this function is called from scheduler
  219. deletes backups older than 7 days
  220. takes backup"""
  221. odb = new_backup(older_than, ignore_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, force=force, verbose=verbose)
  222. return odb
  223. def new_backup(older_than=6, ignore_files=False, backup_path_db=None, backup_path_files=None, backup_path_private_files=None, force=False, verbose=False):
  224. delete_temp_backups(older_than = frappe.conf.keep_backups_for_hours or 24)
  225. odb = BackupGenerator(frappe.conf.db_name, frappe.conf.db_name,\
  226. frappe.conf.db_password,
  227. backup_path_db=backup_path_db, backup_path_files=backup_path_files,
  228. backup_path_private_files=backup_path_private_files,
  229. db_host = frappe.db.host,
  230. db_port = frappe.db.port,
  231. db_type = frappe.conf.db_type,
  232. verbose=verbose)
  233. odb.get_backup(older_than, ignore_files, force=force)
  234. return odb
  235. def delete_temp_backups(older_than=24):
  236. """
  237. Cleans up the backup_link_path directory by deleting files older than 24 hours
  238. """
  239. backup_path = get_backup_path()
  240. if os.path.exists(backup_path):
  241. file_list = os.listdir(get_backup_path())
  242. for this_file in file_list:
  243. this_file_path = os.path.join(get_backup_path(), this_file)
  244. if is_file_old(this_file_path, older_than):
  245. os.remove(this_file_path)
  246. def is_file_old(db_file_name, older_than=24):
  247. """
  248. Checks if file exists and is older than specified hours
  249. Returns ->
  250. True: file does not exist or file is old
  251. False: file is new
  252. """
  253. if os.path.isfile(db_file_name):
  254. from datetime import timedelta
  255. #Get timestamp of the file
  256. file_datetime = datetime.fromtimestamp\
  257. (os.stat(db_file_name).st_ctime)
  258. if datetime.today() - file_datetime >= timedelta(hours = older_than):
  259. if _verbose:
  260. print("File is old")
  261. return True
  262. else:
  263. if _verbose:
  264. print("File is recent")
  265. return False
  266. else:
  267. if _verbose:
  268. print("File does not exist")
  269. return True
  270. def get_backup_path():
  271. backup_path = frappe.utils.get_site_path(conf.get("backup_path", "private/backups"))
  272. return backup_path
  273. def backup(with_files=False, backup_path_db=None, backup_path_files=None, quiet=False):
  274. "Backup"
  275. odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, force=True)
  276. return {
  277. "backup_path_db": odb.backup_path_db,
  278. "backup_path_files": odb.backup_path_files,
  279. "backup_path_private_files": odb.backup_path_private_files
  280. }
  281. if __name__ == "__main__":
  282. """
  283. is_file_old db_name user password db_host db_type db_port
  284. get_backup db_name user password db_host db_type db_port
  285. """
  286. import sys
  287. cmd = sys.argv[1]
  288. db_type = 'mariadb'
  289. try:
  290. db_type = sys.argv[6]
  291. except IndexError:
  292. pass
  293. db_port = 3306
  294. try:
  295. db_port = int(sys.argv[7])
  296. except IndexError:
  297. pass
  298. if cmd == "is_file_old":
  299. odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] or "localhost", db_type=db_type, db_port=db_port)
  300. is_file_old(odb.db_file_name)
  301. if cmd == "get_backup":
  302. odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] or "localhost", db_type=db_type, db_port=db_port)
  303. odb.get_backup()
  304. if cmd == "take_dump":
  305. odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] or "localhost", db_type=db_type, db_port=db_port)
  306. odb.take_dump()
  307. if cmd == "send_email":
  308. odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] or "localhost", db_type=db_type, db_port=db_port)
  309. odb.send_email("abc.sql.gz")
  310. if cmd == "delete_temp_backups":
  311. delete_temp_backups()