面板版本:宝塔Linux面板11.3.0
系统版本:Debian 12
问题描述:
我的后端服务使用了多个不同的 Schema,但不使用默认的名为 public 的 schema.
在计划任务中创建的 备份pgsql数据库[所有] 任务,执行时只得到了一个空的zip压缩包,内部只有个空的sql文件。
我看了一下宝塔的代码,在 class/panelBackup.py 中定义的 pgsql_backup_database 函数是负责执行该备份操作的,但是看上去没有考虑周全。
- db_name = db_find["name"]
- isinstance # <-- 1479行
- db_user = "postgres"
- db_host = "127.0.0.1"
复制代码 1470行莫名其妙地出现一行 isinstance 不知道是何用意。
- if "ALL" in table_list:
- tb_l = pgsql_obj.query("SELECT tablename FROM pg_tables WHERE schemaname = 'public';")
- if isinstance(tb_l, list) and tb_l:
- table_list = [i[0] for i in tb_l]
复制代码 1523行的SQL, 限定了查找表时只查 public 下的表。而且,如果tb_l 为空,那么 table_list 就仍是 ['ALL'],没有把其中的 'ALL' 移除。
- # if storage_type == "db": # 导出单个文件 <---- 1560行
- # file_name = file_name + ".sql.gz"
- # backup_path = os.path.join(db_backup_dir, file_name)
- # table_shell = ""
- # if len(table_list) != 0:
- # table_shell = "--table='" + "' --table='".join(table_list) + "'"
- # shell += " {table_shell} | gzip > '{backup_path}'".format(table_shell=table_shell, backup_path=backup_path)
- # public.ExecShell(shell, env={"PGPASSWORD": db_password})
- # else: # 按表导出
- export_dir = os.path.join(db_backup_dir, file_name)
- if not os.path.isdir(export_dir):
- os.makedirs(export_dir)
复制代码 1560行处的 storage_type 判断被注释掉了,说明 pgsql的备份任务只支持 public schema 下的表的按表导出。
如果存在与数据库用户名同名的schema, 其中有和 public 中同名的表,
由于 pg_dump 默认使用数据库的默认 search_path(通常是 "$user", public),$user 会更优先,
那就会导致备份的表不是 public 下的,这显然应该注意。最好显式指定要备份的表的 schema。
我的目的是想备份整个数据库,所以简单照抄了比较完善的 mysql 的备份逻辑,将这个函数改为如下代码:
- # pgsql 备份数据库
- def pgsql_backup_database(self, db_find: dict, args: dict) -> Tuple[bool, str]:
- from databaseModel.pgsqlModel import panelPgsql
-
- storage_type = args.get("storage_type", "db") # 备份的文件数量, 按照数据库 | 按照表
- table_list = args.get("table_list", []) # 备份的集合
-
- db_name = db_find["name"]
- db_user = "postgres"
- db_host = "127.0.0.1"
- if db_find["db_type"] == 0:
- db_port = panelPgsql.get_config_options("port", int, 5432)
-
- t_path = os.path.join(public.get_panel_path(), "data/postgresAS.json")
- if not os.path.isfile(t_path):
- error_msg = "管理员密码未设置!"
- self.echo_error(error_msg)
- return False, error_msg
- db_password = json.loads(public.readFile(t_path)).get("password", "")
- if not db_password:
- error_msg = "数据库密码为空!请先设置数据库密码!"
- self.echo_error(error_msg)
- return False, error_msg
-
- elif db_find["db_type"] == 1:
- # 远程数据库
- conn_config = json.loads(db_find["conn_config"])
- db_host = conn_config["db_host"]
- db_port = conn_config["db_port"]
- db_user = conn_config["db_user"]
- db_password = conn_config["db_password"]
- elif db_find["db_type"] == 2:
- conn_config = public.M("database_servers").where("id=? AND LOWER(db_type)=LOWER('pgsql')", db_find["sid"]).find()
- db_host = conn_config["db_host"]
- db_port = conn_config["db_port"]
- db_user = conn_config["db_user"]
- db_password = conn_config["db_password"]
- else:
- error_msg = "未知的数据库类型"
- self.echo_error(error_msg)
- return False, error_msg
-
- pgsql_obj = panelPgsql().set_host(host=db_host, port=db_port, database=db_name, user=db_user, password=db_password)
- status, err_msg = pgsql_obj.connect()
- if status is False:
- error_msg = "连接数据库[{}:{}]失败".format(db_host, int(db_port))
- self.echo_error(error_msg)
- return False, error_msg
-
- db_size = 0
- db_data = pgsql_obj.query("SELECT pg_database_size('{}') AS database_size;".format(db_name))
- if isinstance(db_data, list) and len(db_data) != 0:
- db_size = db_data[0][0]
-
- if db_size == 0:
- error_msg = '指定数据库 `{}` 没有任何数据!'.format(db_name)
- self.echo_error(error_msg)
- return False, error_msg
-
- try:
- if "ALL" in table_list:
- table_list=[]
- # tb_l = pgsql_obj.query("SELECT tablename FROM pg_tables WHERE schemaname = 'public';")
- # if isinstance(tb_l, list) and tb_l:
- # table_list = [i[0] for i in tb_l]
- # else:
- # table_list=[]
- except:
- table_list=[]
-
- self.echo_info('备份PgSQL数据库:{}'.format(db_name))
- self.echo_info("数据库大小:{}".format(public.to_size(db_size)))
- self.echo_info("备份的table_list:{}".format(table_list))
- self.echo_info("备份的类型:{}".format(storage_type))
-
- disk_path, disk_free, disk_inode = self.get_disk_free(self._PGSQL_BACKUP_DIR)
- self.echo_info("分区{}可用磁盘空间为:{},可用Inode为:{}".format(disk_path, public.to_size(disk_free), disk_inode))
- if disk_path:
- if disk_free < db_size:
- error_msg = "目标分区可用的磁盘空间小于{},无法完成备份,请增加磁盘容量,或在设置页面更改默认备份目录!".format(public.to_size(db_size))
- self.echo_error(error_msg)
- return False, error_msg
- if disk_inode < self._inode_min:
- error_msg = "目标分区可用的Inode小于{},无法完成备份,请增加磁盘容量,或在设置页面更改默认备份目录!".format(self._inode_min)
- self.echo_error(error_msg)
- return False, error_msg
- stime = time.time()
- self.echo_info("开始导出数据库:{}".format(public.format_date(times=stime)))
- # 调用 get_backup_dir 函数来获取备份目录的路径
- pgsql_backup_dir = self.get_backup_dir(db_find, args, "pgsql")
- # 使用获取的路径来构建备份文件的路径
- db_backup_dir = os.path.join(pgsql_backup_dir, db_name)
- if not os.path.exists(db_backup_dir):
- os.makedirs(db_backup_dir)
-
- file_name = "{db_name}_{backup_time}_pgsql_data".format(db_name=db_name, backup_time=time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()))
-
- shell = "'{pgdump_bin}' --host='{db_host}' --port={db_port} --username='{db_user}' --dbname='{db_name}' --clean".format(
- pgdump_bin=self._PGDUMP_BIN,
- db_host=db_host,
- db_port=int(db_port),
- db_user=db_user,
- db_name=db_name,
- )
-
- if storage_type == "db": # 导出单个文件
- if not os.path.exists("/usr/bin/gzip") and not os.path.exists("/bin/gzip") and not os.path.exists("/usr/sbin/gzip"):
- self.echo_info("备份异常!压缩工具gzip不存在,请在终端执行安装后再执行备份")
- if os.path.exists("/usr/bin/apt-get"):
- self.echo_info("安装命令:apt-get install gzip -y")
- elif os.path.exists("/usr/bin/yum"):
- self.echo_info("安装命令:yum install gzip -y")
- return False, "gzip命令不存在,请先安装gzip"
- file_name = file_name + ".sql.gz"
- backup_path = os.path.join(db_backup_dir, file_name)
- table_shell = ""
- if len(table_list) != 0:
- table_shell = "--table='" + "' --table='".join(table_list) + "'"
-
- # shell += " {table_shell} | gzip > '{backup_path}'".format(table_shell=table_shell, backup_path=backup_path)
- shell += " {table_shell} 2> '{err_log}' | gzip > '{backup_path}'".format(table_shell=table_shell, err_log=self._err_log, backup_path=backup_path)
- self.echo_info("备份语句:{}".format(shell))
- public.ExecShell(shell, env={"PGPASSWORD": db_password})
- else: # 按表导出
- export_dir = os.path.join(db_backup_dir, file_name)
- if not os.path.isdir(export_dir):
- os.makedirs(export_dir)
-
- for table_name in table_list:
- tb_backup_path = os.path.join(export_dir, "{table_name}.sql".format(table_name=table_name))
- tb_shell = shell + " --table='{table_name}' > '{tb_backup_path}'".format(table_name=table_name, tb_backup_path=tb_backup_path)
- public.ExecShell(tb_shell, env={"PGPASSWORD": db_password})
- backup_path = "{export_dir}.zip".format(export_dir=export_dir)
- public.ExecShell("cd '{backup_dir}' && zip -m '{backup_path}' -r '{file_name}'".format(backup_dir=db_backup_dir, backup_path=backup_path, file_name=file_name))
- if not os.path.exists(backup_path):
- public.ExecShell("rm -rf {}", format(export_dir))
-
- # public.ExecShell(shell, env={"PGPASSWORD": db_password})
- if not os.path.exists(backup_path):
- error_msg = "数据库备份失败!"
- self.echo_error(error_msg)
- self.echo_info(public.readFile(self._err_log))
- return False, error_msg
- gz_size = os.path.getsize(backup_path)
- # self.check_disk_space(gz_size,self._PGSQL_BACKUP_DIR,type=1)
- self.echo_info("数据库备份完成,耗时{:.2f}秒,压缩包大小:{}".format(time.time() - stime, public.to_size(gz_size)))
- return True, backup_path
-
复制代码 经测试可以正常备份。 但这毕竟是临时改的面板的代码,写法也没怎么推敲,希望官方能重视起来这种基础的备份功能,毕竟谁也不希望自己设定的备份任务只能得到一堆空白压缩包。
|
|