用户bilsee 发表于 2025-12-2 23:12:04

【已解答】计划任务中的数据库备份,对于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
    经测试可以正常备份。 但这毕竟是临时改的面板的代码,写法也没怎么推敲,希望官方能重视起来这种基础的备份功能,毕竟谁也不希望自己设定的备份任务只能得到一堆空白压缩包。





阿珂 发表于 2025-12-3 16:32:40

感谢反馈,下个版本会进行处理

acks 发表于 2025-12-5 21:43:53

跟帖一个,看咱们这边后续有修复这个问题的计划吗:https://www.bt.cn/bbs/thread-151737-1-1.html
同名远程数据库的问题

用户bilsee 发表于 2025-12-5 22:28:14

阿珂 发表于 2025-12-3 16:32
感谢反馈,下个版本会进行处理

:lol 这不得奖励点宝塔币?

阿珂 发表于 2025-12-6 17:15:39

奖励100,已发放到账户
页: [1]
查看完整版本: 【已解答】计划任务中的数据库备份,对于PgSQL,无法备份...