前提

Python3环境

安装百度云第三方库bypy

推荐Linux,Windows下应该也可以(博主没测试过)

脚本

大体分两个文件,一个是包装了bypy的自用类,一个是实际供命令行调用的主程序文件。

直接上代码,很多逻辑是博主自己用的,请自行根据需要删减或修改。

特别是发送短信提醒的功能,这个博主是使用了腾讯云的短信服务。读者请无视。(自行删除)

主程序

文件名:backupBaota.py (可以随意修改)

文件位置:/home/XXX/mytools/ (用户home目录下的mytools子目录,理论上任意文件夹都可以,不过博主没测试过)

#!/usr/bin/python
#-*- coding:utf-8 -*-
import os,sys
import logging
import requests,json
import time
import socket, getpass
from myLib import Backup2BDY
from myLib import MySmsLib

# 本程序用于备份宝塔面板的定时备份,包括网站和数据库(宝塔面板设置每日一次备份,本程序在cron里也设置每日一次即可)
# 本程序需要Python3,并安装百度云命令行工具bypy
# crontab例子(注意cron命令中的logs文件夹需要提前准备好)
# 45 15 * * * cd ~/mytools/ ; python3 ~/mytools/backupBaota.py >>~/mytools/logs/backupBaota.std.log 2>&1

currentPath = sys.path[0] + "/"

# 日志输出配置
LOG_FILE = currentPath + "logs/backupBaota.log"
LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s"
DATE_FORMAT = "%Y/%m/%d %H:%M:%S"
logging.basicConfig(filename=LOG_FILE, level=logging.INFO, format=LOG_FORMAT, datefmt=DATE_FORMAT)
#logging.basicConfig(level=logging.INFO, format=LOG_FORMAT, datefmt=DATE_FORMAT)

# 保留多少份最新备份
KEEP_DAYS = 30

# 用来取得文件名中的日期部分的正则表达式(用于保留指定份数的最新文件)
# Db_mlc_20210502_150501.sql.gz
# Web_mango.natappvip.cc_20210502_150002.tar.gz
DATE_REGX = r'.*(\d{8})_\d{6}.*'

# 百度云远程目录
BDY_ROOT = '/raspiBackup/wwwBackup/'
BDY_PATH_SITE = BDY_ROOT + 'site/'
BDY_PATH_DATABASE = BDY_ROOT + 'database/'

# 其他备份文件,一般是程序文件本身和日志文件,本地文件和远程路径成对设置即可
other_backup_list = []
other_backup_list.append([currentPath + "backupBaota.py", BDY_ROOT + 'tool/backupBaota.py'])
other_backup_list.append([currentPath + 'myLib/Backup2BDY.py', BDY_ROOT + 'tool/myLib/Backup2BDY.py'])
other_backup_list.append([currentPath + 'myLib/MySmsLib.py', BDY_ROOT + 'tool/myLib/MySmsLib.py'])
other_backup_list.append([LOG_FILE, BDY_ROOT + 'backupBaota.log'])

# 配置完成,以下基本不用修改

hostName = socket.gethostname()
execUser = getpass.getuser()
try:
    timeStart = time.time()
    logging.info("备份开始-----------------------------------------------------")
    logging.info("运行环境: Hostname [%s] User [%s]", hostName, execUser)

    # 发送提醒短信用工具类(不需要可注释掉)
    sms = MySmsLib.MySms()

    bdy = Backup2BDY.Backup2BDY()

    logging.info("开始备份database...")
    # 宝塔备份文件夹使用的是默认设置如果有变化需要修改
    bdy.backupFolder(localPath='/www/backup/database/', remotePath=BDY_PATH_DATABASE, \
                        keepDays=KEEP_DAYS, dateRegx=DATE_REGX)

    logging.info("开始备份site...")
    bdy.backupFolder(localPath='/www/backup/site/', remotePath=BDY_PATH_SITE, \
                        keepDays=KEEP_DAYS, dateRegx=DATE_REGX)

    logging.info("最后备份其他文件(覆盖)...")

    for row in other_backup_list:
        bdy.backupFile(row[0], row[1])
  
    # logging.info("备份成功,发送提醒短信...")
    # timeEnd = time.time()
    # ret = sms.send947195(hostName, "[备份宝塔站点]", "成功", "%d秒"%(timeEnd-timeStart), "无")
    # logging.info("提醒短信发送完毕。")
  
    logging.info("备份完成,程序终止。")

except Exception as err:
    logging.error("出现未知错误\n{0}".format(err))
    logging.info("发送提醒短信...")
    timeEnd = time.time()
    ret = sms.send947195(hostName, "[备份宝塔站点]", "失败。", "%d秒"%(timeEnd-timeStart), "无")
    logging.info("提醒短信发送完毕,程序终止。")




包装类

文件名:Backup2BDY.py

文件位置:/home/XXX/mytools/myLib/ (主程序所在文件夹下的 myLib 子文件夹)

import os
import logging
import re
import shutil
from bypy import ByPy

class Backup2BDY:
  
    # 日志输出配置
    # LOG_FILE = "./Up2BDY.log"
    # LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s"
    # DATE_FORMAT = "%Y/%m/%d %H:%M:%S"
    # logging.basicConfig(filename=LOG_FILE, level=logging.INFO, format=LOG_FORMAT, datefmt=DATE_FORMAT)

    # 获取远程目录下的文件名列表
    def __getRemoteFileList(self, remotePath):

        # 通过os.popen来执行bypy以获取标准输出内容(os.popen返回的其实是一个文件对象)
        fileNameList = []
        isIntroLine = True
        retFile = os.popen("python3 -m bypy list " + remotePath + " '$f'")
        for row in retFile:
            if (not isIntroLine):
                # 已跳过介绍部分文本,开始接收文件名
                fileNameList.append(row.replace('\n', ''))
            # 跳过输出结果开头的部分(MD5码不对之类的消息)
            # 直到输出/app/bypy/...才是真正需要的数据
            if (row.startswith("/app")):
                isIntroLine = False

        retFile.close()
        return fileNameList

    # 取得文件列表中的所有日期一览,倒序排列
    def __getDateListByFileNameList(self, fileNameList, dateRegx ):
        # 获取所有文件名中的日期,宝塔的站点和数据库备份文件名中会包含yyyymmdd_hhmmss
        dateList = []
        for fileName in fileNameList:
            matchObj = re.match( dateRegx, fileName)
            date = matchObj.group(1)
            if (date not in dateList):
                dateList.append(date)

        # 倒序排列
        dateList.sort(reverse=True)
        return dateList

    # 备份文件夹
    def backupFolder(self, localPath, remotePath, keepDays, dateRegx=r'.*(\d{8})_\d{6}.*'):

        # logging.info("test:localPath:%s, remotePath:%s, keepDays:%s, dateRegx:%s", localPath, remotePath, keepDays, dateRegx)
        # return

        # 定义百度云模块
        bp=ByPy()
  
        logging.info("本地文件夹:%s", localPath)

        logging.info("检查云盘中有无过期备份,只保留最新%d天(份)备份。", keepDays)
        logging.info("取得云盘文件夹「%s」中的文件列表...", remotePath)
        fileNameList = self.__getRemoteFileList(remotePath)

        # 取得文件列表中的所有日期一览,倒序排列
        dateList = self.__getDateListByFileNameList(fileNameList, dateRegx)
        logging.info("当前云盘上存在的备份日期列表")
        for date in dateList:
            logging.info("%s", date)
  
        # 根据定义的保留备份天数计算过期日期
        # 比如存在如下日期的文件
        #   20210415
        #   20210412
        #   20210407
        #   20210329
        #   20210203
        #    当备份天数=2,过期日期=20210407
        #    当备份天数=3,过期日期=20210329
        #    当备份天数>=5,没有过期文件
        if (keepDays < len(dateList)):
            outdate = dateList[keepDays]
            logging.info("存在过期备份,早于或等于日期「%s」的备份将被删除。", outdate)
            # 再次循环一次所有文件,并进行删除
            for fileName in fileNameList:
                matchObj = re.match( dateRegx, fileName)
                date = matchObj.group(1)
                # 删除过期备份
                if (date <= outdate):
                    # 删除文件不需要获取标准输出的内容,所以这里直接调用模块而不是os.popen
                    retCode = bp.delete(remotePath + fileName)
                    if (retCode == 0):
                        logging.info("过期备份「%s」删除成功。", remotePath + fileName)
                    else:
                        logging.warn("过期备份「%s」删除失败。", remotePath + fileName)
        else:
            logging.info("目前不存在过期备份。")

        # 上传本地文件夹,已存在的话覆盖
        logging.info("上传本地文件夹...")
        bp.upload(localPath, remotePath)

        # 上传完成以后再次取得远程文件文件列表便于检查
        showLastFileCnt = 10
        logging.info("上传完成。当前云盘上的最新%d个备份文件:", showLastFileCnt)
        fileNameList = self.__getRemoteFileList(remotePath)
        dateList = self.__getDateListByFileNameList(fileNameList, dateRegx)
        cnt = 1
        for date in dateList:
            if (cnt <= showLastFileCnt) :
                for fileName in fileNameList :
                    if (cnt <= showLastFileCnt) :
                        if (fileName.find(date) != -1) :
                            logging.info("[%02d] %s", cnt, fileName)
                            cnt = cnt + 1

    # 备份文件
    def backupFile(self, localFile, remoteFile):
        bp=ByPy()
        logging.info("本地文件: %s", localFile)
        logging.info("远程文件: %s", remoteFile)
        logging.info("上传中...")
        bp.upload(localFile, remoteFile)
        logging.info("完成")
Last modification:February 18, 2022
If you think my article is useful to you, please feel free to appreciate