seitime-frappe/frappe/installer.py
Gavin D'souza 3446026555 chore: Update header: license.txt => LICENSE
The license.txt file has been replaced with LICENSE for quite a while
now. INAL but it didn't seem accurate to say "hey, checkout license.txt
although there's no such file". Apart from this, there were
inconsistencies in the headers altogether...this change brings
consistency.
2021-09-03 12:02:59 +05:30

601 lines
17 KiB
Python
Executable file

# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import json
import os
import sys
import frappe
from frappe.defaults import _clear_cache
def _new_site(
db_name,
site,
mariadb_root_username=None,
mariadb_root_password=None,
admin_password=None,
verbose=False,
install_apps=None,
source_sql=None,
force=False,
no_mariadb_socket=False,
reinstall=False,
db_password=None,
db_type=None,
db_host=None,
db_port=None,
new_site=False,
):
"""Install a new Frappe site"""
if not force and os.path.exists(site):
print("Site {0} already exists".format(site))
sys.exit(1)
if no_mariadb_socket and not db_type == "mariadb":
print("--no-mariadb-socket requires db_type to be set to mariadb.")
sys.exit(1)
if not db_name:
import hashlib
db_name = "_" + hashlib.sha1(site.encode()).hexdigest()[:16]
frappe.init(site=site)
from frappe.commands.scheduler import _is_scheduler_enabled
from frappe.utils import get_site_path, scheduler, touch_file
try:
# enable scheduler post install?
enable_scheduler = _is_scheduler_enabled()
except Exception:
enable_scheduler = False
make_site_dirs()
installing = touch_file(get_site_path("locks", "installing.lock"))
install_db(
root_login=mariadb_root_username,
root_password=mariadb_root_password,
db_name=db_name,
admin_password=admin_password,
verbose=verbose,
source_sql=source_sql,
force=force,
reinstall=reinstall,
db_password=db_password,
db_type=db_type,
db_host=db_host,
db_port=db_port,
no_mariadb_socket=no_mariadb_socket,
)
apps_to_install = (
["frappe"] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or [])
)
for app in apps_to_install:
install_app(app, verbose=verbose, set_as_patched=not source_sql)
os.remove(installing)
scheduler.toggle_scheduler(enable_scheduler)
frappe.db.commit()
scheduler_status = (
"disabled" if frappe.utils.scheduler.is_scheduler_disabled() else "enabled"
)
print("*** Scheduler is", scheduler_status, "***")
def install_db(root_login="root", root_password=None, db_name=None, source_sql=None,
admin_password=None, verbose=True, force=0, site_config=None, reinstall=False,
db_password=None, db_type=None, db_host=None, db_port=None, no_mariadb_socket=False):
import frappe.database
from frappe.database import setup_database
if not db_type:
db_type = frappe.conf.db_type or 'mariadb'
make_conf(db_name, site_config=site_config, db_password=db_password, db_type=db_type, db_host=db_host, db_port=db_port)
frappe.flags.in_install_db = True
frappe.flags.root_login = root_login
frappe.flags.root_password = root_password
setup_database(force, source_sql, verbose, no_mariadb_socket)
frappe.conf.admin_password = frappe.conf.admin_password or admin_password
remove_missing_apps()
frappe.db.create_auth_table()
frappe.db.create_global_search_table()
frappe.db.create_user_settings_table()
frappe.flags.in_install_db = False
def install_app(name, verbose=False, set_as_patched=True):
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
from frappe.model.sync import sync_for
from frappe.modules.utils import sync_customizations
from frappe.utils.fixtures import sync_fixtures
frappe.flags.in_install = name
frappe.flags.ignore_in_install = False
frappe.clear_cache()
app_hooks = frappe.get_hooks(app_name=name)
installed_apps = frappe.get_installed_apps()
# install pre-requisites
if app_hooks.required_apps:
for app in app_hooks.required_apps:
install_app(app, verbose=verbose)
frappe.flags.in_install = name
frappe.clear_cache()
if name not in frappe.get_all_apps():
raise Exception("App not in apps.txt")
if name in installed_apps:
frappe.msgprint(frappe._("App {0} already installed").format(name))
return
print("\nInstalling {0}...".format(name))
if name != "frappe":
frappe.only_for("System Manager")
for before_install in app_hooks.before_install or []:
out = frappe.get_attr(before_install)()
if out==False:
return
if name != "frappe":
add_module_defs(name)
sync_for(name, force=True, sync_everything=True, verbose=verbose, reset_permissions=True)
add_to_installed_apps(name)
frappe.get_doc('Portal Settings', 'Portal Settings').sync_menu()
if set_as_patched:
set_all_patches_as_completed(name)
for after_install in app_hooks.after_install or []:
frappe.get_attr(after_install)()
sync_jobs()
sync_fixtures(name)
sync_customizations(name)
for after_sync in app_hooks.after_sync or []:
frappe.get_attr(after_sync)() #
frappe.flags.in_install = False
def add_to_installed_apps(app_name, rebuild_website=True):
installed_apps = frappe.get_installed_apps()
if not app_name in installed_apps:
installed_apps.append(app_name)
frappe.db.set_global("installed_apps", json.dumps(installed_apps))
frappe.db.commit()
if frappe.flags.in_install:
post_install(rebuild_website)
def remove_from_installed_apps(app_name):
installed_apps = frappe.get_installed_apps()
if app_name in installed_apps:
installed_apps.remove(app_name)
frappe.db.set_value("DefaultValue", {"defkey": "installed_apps"}, "defvalue", json.dumps(installed_apps))
_clear_cache("__global")
frappe.db.commit()
if frappe.flags.in_install:
post_install()
def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False):
"""Remove app and all linked to the app's module with the app from a site."""
import click
site = frappe.local.site
# dont allow uninstall app if not installed unless forced
if not force:
if app_name not in frappe.get_installed_apps():
click.secho(f"App {app_name} not installed on Site {site}", fg="yellow")
return
print(f"Uninstalling App {app_name} from Site {site}...")
if not dry_run and not yes:
confirm = click.confirm(
"All doctypes (including custom), modules related to this app will be"
" deleted. Are you sure you want to continue?"
)
if not confirm:
return
if not (dry_run or no_backup):
from frappe.utils.backups import scheduled_backup
print("Backing up...")
scheduled_backup(ignore_files=True)
frappe.flags.in_uninstall = True
drop_doctypes = []
modules = frappe.get_all("Module Def", filters={"app_name": app_name}, pluck="name")
for module_name in modules:
print(f"Deleting Module '{module_name}'")
for doctype in frappe.get_all(
"DocType", filters={"module": module_name}, fields=["name", "issingle"]
):
print(f"* removing DocType '{doctype.name}'...")
if not dry_run:
frappe.delete_doc("DocType", doctype.name, ignore_on_trash=True)
if not doctype.issingle:
drop_doctypes.append(doctype.name)
linked_doctypes = frappe.get_all(
"DocField", filters={"fieldtype": "Link", "options": "Module Def"}, fields=["parent"]
)
ordered_doctypes = ["Workspace", "Report", "Page", "Web Form"]
all_doctypes_with_linked_modules = ordered_doctypes + [
doctype.parent
for doctype in linked_doctypes
if doctype.parent not in ordered_doctypes
]
doctypes_with_linked_modules = [
x for x in all_doctypes_with_linked_modules if frappe.db.exists("DocType", x)
]
for doctype in doctypes_with_linked_modules:
for record in frappe.get_all(doctype, filters={"module": module_name}, pluck="name"):
print(f"* removing {doctype} '{record}'...")
if not dry_run:
frappe.delete_doc(doctype, record, ignore_on_trash=True, force=True)
print(f"* removing Module Def '{module_name}'...")
if not dry_run:
frappe.delete_doc("Module Def", module_name, ignore_on_trash=True, force=True)
for doctype in set(drop_doctypes):
print(f"* dropping Table for '{doctype}'...")
if not dry_run:
frappe.db.sql_ddl(f"drop table `tab{doctype}`")
if not dry_run:
remove_from_installed_apps(app_name)
frappe.db.commit()
click.secho(f"Uninstalled App {app_name} from Site {site}", fg="green")
frappe.flags.in_uninstall = False
def post_install(rebuild_website=False):
from frappe.website.utils import clear_website_cache
if rebuild_website:
clear_website_cache()
init_singles()
frappe.db.commit()
frappe.clear_cache()
def set_all_patches_as_completed(app):
patch_path = os.path.join(frappe.get_pymodule_path(app), "patches.txt")
if os.path.exists(patch_path):
for patch in frappe.get_file_items(patch_path):
frappe.get_doc({
"doctype": "Patch Log",
"patch": patch
}).insert(ignore_permissions=True)
frappe.db.commit()
def init_singles():
singles = [single['name'] for single in frappe.get_all("DocType", filters={'issingle': True})]
for single in singles:
if not frappe.db.get_singles_dict(single):
doc = frappe.new_doc(single)
doc.flags.ignore_mandatory=True
doc.flags.ignore_validate=True
doc.save()
def make_conf(db_name=None, db_password=None, site_config=None, db_type=None, db_host=None, db_port=None):
site = frappe.local.site
make_site_config(db_name, db_password, site_config, db_type=db_type, db_host=db_host, db_port=db_port)
sites_path = frappe.local.sites_path
frappe.destroy()
frappe.init(site, sites_path=sites_path)
def make_site_config(db_name=None, db_password=None, site_config=None, db_type=None, db_host=None, db_port=None):
frappe.create_folder(os.path.join(frappe.local.site_path))
site_file = get_site_config_path()
if not os.path.exists(site_file):
if not (site_config and isinstance(site_config, dict)):
site_config = get_conf_params(db_name, db_password)
if db_type:
site_config['db_type'] = db_type
if db_host:
site_config['db_host'] = db_host
if db_port:
site_config['db_port'] = db_port
with open(site_file, "w") as f:
f.write(json.dumps(site_config, indent=1, sort_keys=True))
def update_site_config(key, value, validate=True, site_config_path=None):
"""Update a value in site_config"""
if not site_config_path:
site_config_path = get_site_config_path()
with open(site_config_path, "r") as f:
site_config = json.loads(f.read())
# In case of non-int value
if value in ('0', '1'):
value = int(value)
# boolean
if value == 'false': value = False
if value == 'true': value = True
# remove key if value is None
if value == "None":
if key in site_config:
del site_config[key]
else:
site_config[key] = value
with open(site_config_path, "w") as f:
f.write(json.dumps(site_config, indent=1, sort_keys=True))
if hasattr(frappe.local, "conf"):
frappe.local.conf[key] = value
def get_site_config_path():
return os.path.join(frappe.local.site_path, "site_config.json")
def get_conf_params(db_name=None, db_password=None):
if not db_name:
db_name = input("Database Name: ")
if not db_name:
raise Exception("Database Name Required")
if not db_password:
from frappe.utils import random_string
db_password = random_string(16)
return {"db_name": db_name, "db_password": db_password}
def make_site_dirs():
for dir_path in [
os.path.join("public", "files"),
os.path.join("private", "backups"),
os.path.join("private", "files"),
"error-snapshots",
"locks",
"logs",
]:
path = frappe.get_site_path(dir_path)
os.makedirs(path, exist_ok=True)
def add_module_defs(app):
modules = frappe.get_module_list(app)
for module in modules:
d = frappe.new_doc("Module Def")
d.app_name = app
d.module_name = module
d.save(ignore_permissions=True)
def remove_missing_apps():
import importlib
apps = ('frappe_subscription', 'shopping_cart')
installed_apps = json.loads(frappe.db.get_global("installed_apps") or "[]")
for app in apps:
if app in installed_apps:
try:
importlib.import_module(app)
except ImportError:
installed_apps.remove(app)
frappe.db.set_global("installed_apps", json.dumps(installed_apps))
def extract_sql_from_archive(sql_file_path):
"""Return the path of an SQL file if the passed argument is the path of a gzipped
SQL file or an SQL file path. The path may be absolute or relative from the bench
root directory or the sites sub-directory.
Args:
sql_file_path (str): Path of the SQL file
Returns:
str: Path of the decompressed SQL file
"""
from frappe.utils import get_bench_relative_path
sql_file_path = get_bench_relative_path(sql_file_path)
# Extract the gzip file if user has passed *.sql.gz file instead of *.sql file
if sql_file_path.endswith('sql.gz'):
decompressed_file_name = extract_sql_gzip(sql_file_path)
else:
decompressed_file_name = sql_file_path
return decompressed_file_name
def extract_sql_gzip(sql_gz_path):
import subprocess
try:
# dvf - decompress, verbose, force
original_file = sql_gz_path
decompressed_file = original_file.rstrip(".gz")
cmd = 'gzip -dvf < {0} > {1}'.format(original_file, decompressed_file)
subprocess.check_call(cmd, shell=True)
except:
raise
return decompressed_file
def extract_files(site_name, file_path):
import shutil
import subprocess
from frappe.utils import get_bench_relative_path
file_path = get_bench_relative_path(file_path)
# Need to do frappe.init to maintain the site locals
frappe.init(site=site_name)
abs_site_path = os.path.abspath(frappe.get_site_path())
# Copy the files to the parent directory and extract
shutil.copy2(os.path.abspath(file_path), abs_site_path)
# Get the file name splitting the file path on
tar_name = os.path.split(file_path)[1]
tar_path = os.path.join(abs_site_path, tar_name)
try:
if file_path.endswith(".tar"):
subprocess.check_output(['tar', 'xvf', tar_path, '--strip', '2'], cwd=abs_site_path)
elif file_path.endswith(".tgz"):
subprocess.check_output(['tar', 'zxvf', tar_path, '--strip', '2'], cwd=abs_site_path)
except:
raise
finally:
frappe.destroy()
return tar_path
def is_downgrade(sql_file_path, verbose=False):
"""checks if input db backup will get downgraded on current bench"""
# This function is only tested with mariadb
# TODO: Add postgres support
if frappe.conf.db_type not in (None, "mariadb"):
return False
from semantic_version import Version
head = "INSERT INTO `tabInstalled Application` VALUES"
with open(sql_file_path) as f:
for line in f:
if head in line:
# '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')
line = line.strip().lstrip(head).rstrip(";").strip()
app_rows = frappe.safe_eval(line)
# check if iterable consists of tuples before trying to transform
apps_list = app_rows if all(isinstance(app_row, (tuple, list, set)) for app_row in app_rows) else (app_rows, )
# 'all_apps' (list) format: [('frappe', '12.x.x-develop ()', 'develop'), ('your_custom_app', '0.0.1', 'master')]
all_apps = [ x[-3:] for x in apps_list ]
for app in all_apps:
app_name = app[0]
app_version = app[1].split(" ")[0]
if app_name == "frappe":
try:
current_version = Version(frappe.__version__)
backup_version = Version(app_version[1:] if app_version[0] == "v" else app_version)
except ValueError:
return False
downgrade = backup_version > current_version
if verbose and downgrade:
print("Your site will be downgraded from Frappe {0} to {1}".format(current_version, backup_version))
return downgrade
def is_partial(sql_file_path):
with open(sql_file_path) as f:
header = " ".join(f.readline() for _ in range(5))
if "Partial Backup" in header:
return True
return False
def partial_restore(sql_file_path, verbose=False):
sql_file = extract_sql_from_archive(sql_file_path)
if frappe.conf.db_type in (None, "mariadb"):
from frappe.database.mariadb.setup_db import import_db_from_sql
elif frappe.conf.db_type == "postgres":
from frappe.database.postgres.setup_db import import_db_from_sql
import warnings
from click import style
warn = style(
"Delete the tables you want to restore manually before attempting"
" partial restore operation for PostreSQL databases",
fg="yellow"
)
warnings.warn(warn)
import_db_from_sql(source_sql=sql_file, verbose=verbose)
# Removing temporarily created file
if sql_file != sql_file_path:
os.remove(sql_file)
def validate_database_sql(path, _raise=True):
"""Check if file has contents and if DefaultValue table exists
Args:
path (str): Path of the decompressed SQL file
_raise (bool, optional): Raise exception if invalid file. Defaults to True.
"""
empty_file = False
missing_table = True
error_message = ""
if not os.path.getsize(path):
error_message = f"{path} is an empty file!"
empty_file = True
# dont bother checking if empty file
if not empty_file:
with open(path, "r") as f:
for line in f:
if 'tabDefaultValue' in line:
missing_table = False
break
if missing_table:
error_message = "Table `tabDefaultValue` not found in file."
if error_message:
import click
click.secho(error_message, fg="red")
if _raise and (missing_table or empty_file):
raise frappe.InvalidDatabaseFile