【已解答】计划任务中的数据库备份,对于PgSQL,无法备份...
面板版本:宝塔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 = 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:
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
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 = 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
经测试可以正常备份。 但这毕竟是临时改的面板的代码,写法也没怎么推敲,希望官方能重视起来这种基础的备份功能,毕竟谁也不希望自己设定的备份任务只能得到一堆空白压缩包。
感谢反馈,下个版本会进行处理 跟帖一个,看咱们这边后续有修复这个问题的计划吗:https://www.bt.cn/bbs/thread-151737-1-1.html
同名远程数据库的问题 阿珂 发表于 2025-12-3 16:32
感谢反馈,下个版本会进行处理
:lol 这不得奖励点宝塔币? 奖励100,已发放到账户
页:
[1]