Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 
 
 

678 lignes
19 KiB

  1. # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  2. # License: MIT. See LICENSE
  3. import json
  4. import os
  5. import sys
  6. from collections import OrderedDict
  7. from typing import List, Dict
  8. import frappe
  9. from frappe.defaults import _clear_cache
  10. def _new_site(
  11. db_name,
  12. site,
  13. mariadb_root_username=None,
  14. mariadb_root_password=None,
  15. admin_password=None,
  16. verbose=False,
  17. install_apps=None,
  18. source_sql=None,
  19. force=False,
  20. no_mariadb_socket=False,
  21. reinstall=False,
  22. db_password=None,
  23. db_type=None,
  24. db_host=None,
  25. db_port=None,
  26. new_site=False,
  27. ):
  28. """Install a new Frappe site"""
  29. from frappe.commands.scheduler import _is_scheduler_enabled
  30. from frappe.utils import get_site_path, scheduler, touch_file
  31. if not force and os.path.exists(site):
  32. print("Site {0} already exists".format(site))
  33. sys.exit(1)
  34. if no_mariadb_socket and not db_type == "mariadb":
  35. print("--no-mariadb-socket requires db_type to be set to mariadb.")
  36. sys.exit(1)
  37. frappe.init(site=site)
  38. if not db_name:
  39. import hashlib
  40. db_name = "_" + hashlib.sha1(os.path.realpath(frappe.get_site_path()).encode()).hexdigest()[:16]
  41. try:
  42. # enable scheduler post install?
  43. enable_scheduler = _is_scheduler_enabled()
  44. except Exception:
  45. enable_scheduler = False
  46. make_site_dirs()
  47. installing = touch_file(get_site_path("locks", "installing.lock"))
  48. install_db(
  49. root_login=mariadb_root_username,
  50. root_password=mariadb_root_password,
  51. db_name=db_name,
  52. admin_password=admin_password,
  53. verbose=verbose,
  54. source_sql=source_sql,
  55. force=force,
  56. reinstall=reinstall,
  57. db_password=db_password,
  58. db_type=db_type,
  59. db_host=db_host,
  60. db_port=db_port,
  61. no_mariadb_socket=no_mariadb_socket,
  62. )
  63. apps_to_install = (
  64. ["frappe"] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or [])
  65. )
  66. for app in apps_to_install:
  67. install_app(app, verbose=verbose, set_as_patched=not source_sql)
  68. os.remove(installing)
  69. scheduler.toggle_scheduler(enable_scheduler)
  70. frappe.db.commit()
  71. scheduler_status = (
  72. "disabled" if frappe.utils.scheduler.is_scheduler_disabled() else "enabled"
  73. )
  74. print("*** Scheduler is", scheduler_status, "***")
  75. def install_db(root_login="root", root_password=None, db_name=None, source_sql=None,
  76. admin_password=None, verbose=True, force=0, site_config=None, reinstall=False,
  77. db_password=None, db_type=None, db_host=None, db_port=None, no_mariadb_socket=False):
  78. import frappe.database
  79. from frappe.database import setup_database
  80. if not db_type:
  81. db_type = frappe.conf.db_type or 'mariadb'
  82. make_conf(db_name, site_config=site_config, db_password=db_password, db_type=db_type, db_host=db_host, db_port=db_port)
  83. frappe.flags.in_install_db = True
  84. frappe.flags.root_login = root_login
  85. frappe.flags.root_password = root_password
  86. setup_database(force, source_sql, verbose, no_mariadb_socket)
  87. frappe.conf.admin_password = frappe.conf.admin_password or admin_password
  88. remove_missing_apps()
  89. frappe.db.create_auth_table()
  90. frappe.db.create_global_search_table()
  91. frappe.db.create_user_settings_table()
  92. frappe.flags.in_install_db = False
  93. def install_app(name, verbose=False, set_as_patched=True):
  94. from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
  95. from frappe.model.sync import sync_for
  96. from frappe.modules.utils import sync_customizations
  97. from frappe.utils.fixtures import sync_fixtures
  98. frappe.flags.in_install = name
  99. frappe.flags.ignore_in_install = False
  100. frappe.clear_cache()
  101. app_hooks = frappe.get_hooks(app_name=name)
  102. installed_apps = frappe.get_installed_apps()
  103. # install pre-requisites
  104. if app_hooks.required_apps:
  105. for app in app_hooks.required_apps:
  106. install_app(app, verbose=verbose)
  107. frappe.flags.in_install = name
  108. frappe.clear_cache()
  109. if name not in frappe.get_all_apps():
  110. raise Exception("App not in apps.txt")
  111. if name in installed_apps:
  112. frappe.msgprint(frappe._("App {0} already installed").format(name))
  113. return
  114. print("\nInstalling {0}...".format(name))
  115. if name != "frappe":
  116. frappe.only_for("System Manager")
  117. for before_install in app_hooks.before_install or []:
  118. out = frappe.get_attr(before_install)()
  119. if out is False:
  120. return
  121. if name != "frappe":
  122. add_module_defs(name)
  123. sync_for(name, force=True, reset_permissions=True)
  124. add_to_installed_apps(name)
  125. frappe.get_doc('Portal Settings', 'Portal Settings').sync_menu()
  126. if set_as_patched:
  127. set_all_patches_as_completed(name)
  128. for after_install in app_hooks.after_install or []:
  129. frappe.get_attr(after_install)()
  130. sync_jobs()
  131. sync_fixtures(name)
  132. sync_customizations(name)
  133. for after_sync in app_hooks.after_sync or []:
  134. frappe.get_attr(after_sync)() #
  135. frappe.flags.in_install = False
  136. def add_to_installed_apps(app_name, rebuild_website=True):
  137. installed_apps = frappe.get_installed_apps()
  138. if not app_name in installed_apps:
  139. installed_apps.append(app_name)
  140. frappe.db.set_global("installed_apps", json.dumps(installed_apps))
  141. frappe.db.commit()
  142. if frappe.flags.in_install:
  143. post_install(rebuild_website)
  144. def remove_from_installed_apps(app_name):
  145. installed_apps = frappe.get_installed_apps()
  146. if app_name in installed_apps:
  147. installed_apps.remove(app_name)
  148. frappe.db.set_value("DefaultValue", {"defkey": "installed_apps"}, "defvalue", json.dumps(installed_apps))
  149. _clear_cache("__global")
  150. frappe.db.commit()
  151. if frappe.flags.in_install:
  152. post_install()
  153. def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False):
  154. """Remove app and all linked to the app's module with the app from a site."""
  155. import click
  156. site = frappe.local.site
  157. app_hooks = frappe.get_hooks(app_name=app_name)
  158. # dont allow uninstall app if not installed unless forced
  159. if not force:
  160. if app_name not in frappe.get_installed_apps():
  161. click.secho(f"App {app_name} not installed on Site {site}", fg="yellow")
  162. return
  163. print(f"Uninstalling App {app_name} from Site {site}...")
  164. if not dry_run and not yes:
  165. confirm = click.confirm(
  166. "All doctypes (including custom), modules related to this app will be"
  167. " deleted. Are you sure you want to continue?"
  168. )
  169. if not confirm:
  170. return
  171. if not (dry_run or no_backup):
  172. from frappe.utils.backups import scheduled_backup
  173. print("Backing up...")
  174. scheduled_backup(ignore_files=True)
  175. frappe.flags.in_uninstall = True
  176. for before_uninstall in app_hooks.before_uninstall or []:
  177. frappe.get_attr(before_uninstall)()
  178. modules = frappe.get_all("Module Def", filters={"app_name": app_name}, pluck="name")
  179. drop_doctypes = _delete_modules(modules, dry_run=dry_run)
  180. _delete_doctypes(drop_doctypes, dry_run=dry_run)
  181. if not dry_run:
  182. remove_from_installed_apps(app_name)
  183. frappe.get_single('Installed Applications').update_versions()
  184. frappe.db.commit()
  185. for after_uninstall in app_hooks.after_uninstall or []:
  186. frappe.get_attr(after_uninstall)()
  187. click.secho(f"Uninstalled App {app_name} from Site {site}", fg="green")
  188. frappe.flags.in_uninstall = False
  189. def _delete_modules(modules: List[str], dry_run: bool) -> List[str]:
  190. """ Delete modules belonging to the app and all related doctypes.
  191. Note: All record linked linked to Module Def are also deleted.
  192. Returns: list of deleted doctypes."""
  193. drop_doctypes = []
  194. doctype_link_field_map = _get_module_linked_doctype_field_map()
  195. for module_name in modules:
  196. print(f"Deleting Module '{module_name}'")
  197. for doctype in frappe.get_all(
  198. "DocType", filters={"module": module_name}, fields=["name", "issingle"]
  199. ):
  200. print(f"* removing DocType '{doctype.name}'...")
  201. if not dry_run:
  202. if doctype.issingle:
  203. frappe.delete_doc("DocType", doctype.name, ignore_on_trash=True)
  204. else:
  205. drop_doctypes.append(doctype.name)
  206. _delete_linked_documents(module_name, doctype_link_field_map, dry_run=dry_run)
  207. print(f"* removing Module Def '{module_name}'...")
  208. if not dry_run:
  209. frappe.delete_doc("Module Def", module_name, ignore_on_trash=True, force=True)
  210. return drop_doctypes
  211. def _delete_linked_documents(
  212. module_name: str,
  213. doctype_linkfield_map: Dict[str, str],
  214. dry_run: bool
  215. ) -> None:
  216. """Deleted all records linked with module def"""
  217. for doctype, fieldname in doctype_linkfield_map.items():
  218. for record in frappe.get_all(doctype, filters={fieldname: module_name}, pluck="name"):
  219. print(f"* removing {doctype} '{record}'...")
  220. if not dry_run:
  221. frappe.delete_doc(doctype, record, ignore_on_trash=True, force=True)
  222. def _get_module_linked_doctype_field_map() -> Dict[str, str]:
  223. """ Get all the doctypes which have module linked with them.
  224. returns ordered dictionary with doctype->link field mapping."""
  225. # Hardcoded to change order of deletion
  226. ordered_doctypes = [
  227. ("Workspace", "module"),
  228. ("Report", "module"),
  229. ("Page", "module"),
  230. ("Web Form", "module")
  231. ]
  232. doctype_to_field_map = OrderedDict(ordered_doctypes)
  233. linked_doctypes = frappe.get_all(
  234. "DocField", filters={"fieldtype": "Link", "options": "Module Def"}, fields=["parent", "fieldname"]
  235. )
  236. existing_linked_doctypes = [d for d in linked_doctypes if frappe.db.exists("DocType", d.parent)]
  237. for d in existing_linked_doctypes:
  238. # DocType deletion is handled separately in the end
  239. if d.parent not in doctype_to_field_map and d.parent != "DocType":
  240. doctype_to_field_map[d.parent] = d.fieldname
  241. return doctype_to_field_map
  242. def _delete_doctypes(doctypes: List[str], dry_run: bool) -> None:
  243. for doctype in set(doctypes):
  244. print(f"* dropping Table for '{doctype}'...")
  245. if not dry_run:
  246. frappe.delete_doc("DocType", doctype, ignore_on_trash=True)
  247. frappe.db.sql_ddl(f"DROP TABLE IF EXISTS `tab{doctype}`")
  248. def post_install(rebuild_website=False):
  249. from frappe.website.utils import clear_website_cache
  250. if rebuild_website:
  251. clear_website_cache()
  252. init_singles()
  253. frappe.db.commit()
  254. frappe.clear_cache()
  255. def set_all_patches_as_completed(app):
  256. from frappe.modules.patch_handler import get_patches_from_app
  257. patches = get_patches_from_app(app)
  258. for patch in patches:
  259. frappe.get_doc({
  260. "doctype": "Patch Log",
  261. "patch": patch
  262. }).insert(ignore_permissions=True)
  263. frappe.db.commit()
  264. def init_singles():
  265. singles = [single['name'] for single in frappe.get_all("DocType", filters={'issingle': True})]
  266. for single in singles:
  267. if not frappe.db.get_singles_dict(single):
  268. doc = frappe.new_doc(single)
  269. doc.flags.ignore_mandatory=True
  270. doc.flags.ignore_validate=True
  271. doc.save()
  272. def make_conf(db_name=None, db_password=None, site_config=None, db_type=None, db_host=None, db_port=None):
  273. site = frappe.local.site
  274. make_site_config(db_name, db_password, site_config, db_type=db_type, db_host=db_host, db_port=db_port)
  275. sites_path = frappe.local.sites_path
  276. frappe.destroy()
  277. frappe.init(site, sites_path=sites_path)
  278. def make_site_config(db_name=None, db_password=None, site_config=None, db_type=None, db_host=None, db_port=None):
  279. frappe.create_folder(os.path.join(frappe.local.site_path))
  280. site_file = get_site_config_path()
  281. if not os.path.exists(site_file):
  282. if not (site_config and isinstance(site_config, dict)):
  283. site_config = get_conf_params(db_name, db_password)
  284. if db_type:
  285. site_config['db_type'] = db_type
  286. if db_host:
  287. site_config['db_host'] = db_host
  288. if db_port:
  289. site_config['db_port'] = db_port
  290. with open(site_file, "w") as f:
  291. f.write(json.dumps(site_config, indent=1, sort_keys=True))
  292. def update_site_config(key, value, validate=True, site_config_path=None):
  293. """Update a value in site_config"""
  294. if not site_config_path:
  295. site_config_path = get_site_config_path()
  296. with open(site_config_path, "r") as f:
  297. site_config = json.loads(f.read())
  298. # In case of non-int value
  299. if value in ('0', '1'):
  300. value = int(value)
  301. # boolean
  302. if value == 'false': value = False
  303. if value == 'true': value = True
  304. # remove key if value is None
  305. if value == "None":
  306. if key in site_config:
  307. del site_config[key]
  308. else:
  309. site_config[key] = value
  310. with open(site_config_path, "w") as f:
  311. f.write(json.dumps(site_config, indent=1, sort_keys=True))
  312. if hasattr(frappe.local, "conf"):
  313. frappe.local.conf[key] = value
  314. def get_site_config_path():
  315. return os.path.join(frappe.local.site_path, "site_config.json")
  316. def get_conf_params(db_name=None, db_password=None):
  317. if not db_name:
  318. db_name = input("Database Name: ")
  319. if not db_name:
  320. raise Exception("Database Name Required")
  321. if not db_password:
  322. from frappe.utils import random_string
  323. db_password = random_string(16)
  324. return {"db_name": db_name, "db_password": db_password}
  325. def make_site_dirs():
  326. for dir_path in [
  327. os.path.join("public", "files"),
  328. os.path.join("private", "backups"),
  329. os.path.join("private", "files"),
  330. "error-snapshots",
  331. "locks",
  332. "logs",
  333. ]:
  334. path = frappe.get_site_path(dir_path)
  335. os.makedirs(path, exist_ok=True)
  336. def add_module_defs(app):
  337. modules = frappe.get_module_list(app)
  338. for module in modules:
  339. d = frappe.new_doc("Module Def")
  340. d.app_name = app
  341. d.module_name = module
  342. d.save(ignore_permissions=True)
  343. def remove_missing_apps():
  344. import importlib
  345. apps = ('frappe_subscription', 'shopping_cart')
  346. installed_apps = json.loads(frappe.db.get_global("installed_apps") or "[]")
  347. for app in apps:
  348. if app in installed_apps:
  349. try:
  350. importlib.import_module(app)
  351. except ImportError:
  352. installed_apps.remove(app)
  353. frappe.db.set_global("installed_apps", json.dumps(installed_apps))
  354. def extract_sql_from_archive(sql_file_path):
  355. """Return the path of an SQL file if the passed argument is the path of a gzipped
  356. SQL file or an SQL file path. The path may be absolute or relative from the bench
  357. root directory or the sites sub-directory.
  358. Args:
  359. sql_file_path (str): Path of the SQL file
  360. Returns:
  361. str: Path of the decompressed SQL file
  362. """
  363. from frappe.utils import get_bench_relative_path
  364. sql_file_path = get_bench_relative_path(sql_file_path)
  365. # Extract the gzip file if user has passed *.sql.gz file instead of *.sql file
  366. if sql_file_path.endswith('sql.gz'):
  367. decompressed_file_name = extract_sql_gzip(sql_file_path)
  368. else:
  369. decompressed_file_name = sql_file_path
  370. # convert archive sql to latest compatible
  371. convert_archive_content(decompressed_file_name)
  372. return decompressed_file_name
  373. def convert_archive_content(sql_file_path):
  374. if frappe.conf.db_type == "mariadb":
  375. # ever since mariaDB 10.6, row_format COMPRESSED has been deprecated and removed
  376. # this step is added to ease restoring sites depending on older mariaDB servers
  377. from frappe.utils import random_string
  378. from pathlib import Path
  379. old_sql_file_path = Path(f"{sql_file_path}_{random_string(10)}")
  380. sql_file_path = Path(sql_file_path)
  381. os.rename(sql_file_path, old_sql_file_path)
  382. sql_file_path.touch()
  383. with open(old_sql_file_path) as r, open(sql_file_path, "a") as w:
  384. for line in r:
  385. w.write(line.replace("ROW_FORMAT=COMPRESSED", "ROW_FORMAT=DYNAMIC"))
  386. old_sql_file_path.unlink()
  387. def extract_sql_gzip(sql_gz_path):
  388. import subprocess
  389. try:
  390. original_file = sql_gz_path
  391. decompressed_file = original_file.rstrip(".gz")
  392. cmd = 'gzip --decompress --force < {0} > {1}'.format(original_file, decompressed_file)
  393. subprocess.check_call(cmd, shell=True)
  394. except Exception:
  395. raise
  396. return decompressed_file
  397. def extract_files(site_name, file_path):
  398. import shutil
  399. import subprocess
  400. from frappe.utils import get_bench_relative_path
  401. file_path = get_bench_relative_path(file_path)
  402. # Need to do frappe.init to maintain the site locals
  403. frappe.init(site=site_name)
  404. abs_site_path = os.path.abspath(frappe.get_site_path())
  405. # Copy the files to the parent directory and extract
  406. shutil.copy2(os.path.abspath(file_path), abs_site_path)
  407. # Get the file name splitting the file path on
  408. tar_name = os.path.split(file_path)[1]
  409. tar_path = os.path.join(abs_site_path, tar_name)
  410. try:
  411. if file_path.endswith(".tar"):
  412. subprocess.check_output(['tar', 'xvf', tar_path, '--strip', '2'], cwd=abs_site_path)
  413. elif file_path.endswith(".tgz"):
  414. subprocess.check_output(['tar', 'zxvf', tar_path, '--strip', '2'], cwd=abs_site_path)
  415. except:
  416. raise
  417. finally:
  418. frappe.destroy()
  419. return tar_path
  420. def is_downgrade(sql_file_path, verbose=False):
  421. """checks if input db backup will get downgraded on current bench"""
  422. # This function is only tested with mariadb
  423. # TODO: Add postgres support
  424. if frappe.conf.db_type not in (None, "mariadb"):
  425. return False
  426. from semantic_version import Version
  427. head = "INSERT INTO `tabInstalled Application` VALUES"
  428. with open(sql_file_path) as f:
  429. for line in f:
  430. if head in line:
  431. # 'line' (str) format: ('2056588823','2020-05-11 18:21:31.488367','2020-06-12 11:49:31.079506','Administrator','Administrator',0,'Installed Applications','installed_applications','Installed Applications',1,'frappe','v10.1.71-74 (3c50d5e) (v10.x.x)','v10.x.x'),('855c640b8e','2020-05-11 18:21:31.488367','2020-06-12 11:49:31.079506','Administrator','Administrator',0,'Installed Applications','installed_applications','Installed Applications',2,'your_custom_app','0.0.1','master')
  432. line = line.strip().lstrip(head).rstrip(";").strip()
  433. app_rows = frappe.safe_eval(line)
  434. # check if iterable consists of tuples before trying to transform
  435. apps_list = app_rows if all(isinstance(app_row, (tuple, list, set)) for app_row in app_rows) else (app_rows, )
  436. # 'all_apps' (list) format: [('frappe', '12.x.x-develop ()', 'develop'), ('your_custom_app', '0.0.1', 'master')]
  437. all_apps = [ x[-3:] for x in apps_list ]
  438. for app in all_apps:
  439. app_name = app[0]
  440. app_version = app[1].split(" ")[0]
  441. if app_name == "frappe":
  442. try:
  443. current_version = Version(frappe.__version__)
  444. backup_version = Version(app_version[1:] if app_version[0] == "v" else app_version)
  445. except ValueError:
  446. return False
  447. downgrade = backup_version > current_version
  448. if verbose and downgrade:
  449. print("Your site will be downgraded from Frappe {0} to {1}".format(current_version, backup_version))
  450. return downgrade
  451. def is_partial(sql_file_path):
  452. with open(sql_file_path) as f:
  453. header = " ".join(f.readline() for _ in range(5))
  454. if "Partial Backup" in header:
  455. return True
  456. return False
  457. def partial_restore(sql_file_path, verbose=False):
  458. sql_file = extract_sql_from_archive(sql_file_path)
  459. if frappe.conf.db_type in (None, "mariadb"):
  460. from frappe.database.mariadb.setup_db import import_db_from_sql
  461. elif frappe.conf.db_type == "postgres":
  462. from frappe.database.postgres.setup_db import import_db_from_sql
  463. import warnings
  464. from click import style
  465. warn = style(
  466. "Delete the tables you want to restore manually before attempting"
  467. " partial restore operation for PostreSQL databases",
  468. fg="yellow"
  469. )
  470. warnings.warn(warn)
  471. import_db_from_sql(source_sql=sql_file, verbose=verbose)
  472. # Removing temporarily created file
  473. if sql_file != sql_file_path:
  474. os.remove(sql_file)
  475. def validate_database_sql(path, _raise=True):
  476. """Check if file has contents and if DefaultValue table exists
  477. Args:
  478. path (str): Path of the decompressed SQL file
  479. _raise (bool, optional): Raise exception if invalid file. Defaults to True.
  480. """
  481. empty_file = False
  482. missing_table = True
  483. error_message = ""
  484. if not os.path.getsize(path):
  485. error_message = f"{path} is an empty file!"
  486. empty_file = True
  487. # dont bother checking if empty file
  488. if not empty_file:
  489. with open(path, "r") as f:
  490. for line in f:
  491. if 'tabDefaultValue' in line:
  492. missing_table = False
  493. break
  494. if missing_table:
  495. error_message = "Table `tabDefaultValue` not found in file."
  496. if error_message:
  497. import click
  498. click.secho(error_message, fg="red")
  499. if _raise and (missing_table or empty_file):
  500. raise frappe.InvalidDatabaseFile