Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 
 
 

605 linhas
17 KiB

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