宝塔的自动备份目录功能只有在备份失败才会推送钉钉消息,而且消息不可自定义,于是做了此脚本。代码主要来源于七牛云的SDK,部分参考网上,在此感谢。
#!/usr/bin/env python
# coding:utf-8
# author:feng
# date: 2023/02/04
# runtime:python 3.6
# func:打包压缩文件并上传到七牛云,上传结果通过钉钉推送
# remarks:
# 需要下载七牛云sdk,使用"pip install qiniu"下载即可
# 钉钉机器人需要设置关键词,且推送消息中需要有关键词才能成功推送,本例中设置的关键词是“备份”,可自行修改,
# 但要和机器人保一致
# 代码主要参考七牛SDK,部分参考网上代码,在此感谢。
import os
import re
import json
import requests
from datetime import datetime
from qiniu import Auth, BucketManager, put_file, etag
class BackupSingleFile2Qiniuyun:
def __init__(self):
# 以下参数需要设置
# 操作日志保存完整路径()
self.log_path = r'/BackupLog.txt'
# 待备份目录(或文件)完整路径
self.src_path = '/data/'
# 待上传文件存储路径(待备份文件打包压缩后的文件存储目录)
self.dest_path = '/Backup/'
# access key(七牛云)
self.key = ''
# secret key(七牛云)
self.secret = ''
# 桶名称(七牛云)
self.bucket_name = ''
# 七牛云目录名
self.qny_dir = 'backup/'
# 保留文件份数(本地和七牛云都保存相同的份数)
self.remain_num = 3
# 钉钉机器人Webhook地址
self.web_hook = ''
# 以下参数无需修改
self.q = None
self.bucket = None
# 不用修改
self.host = 'rs.qbox.me'
# 文件名(待备份目录名)
self.file_name = ''
# 获取当前日期
self.now_date = None
# 压缩文件名
self.compressed_file_name = ''
# 正则匹配规则
self.regex = ''
def start_backup(self):
self.replace_file_path()
self.file_name = self.get_need_backup_dir_name()
self.regex = '^' + self.file_name + '-\\d{4}-\\d{2}-\\d{2}\\.tar\\.gz'
self.get_now_date()
self.q = Auth(self.key, self.secret)
self.bucket = BucketManager(self.q)
str_log = self.get_timestamp() + ' 初始化完成!'
print(str_log)
self.write_log(str_log)
# 压缩文件
ret = self.compress_file()
if 0 != ret:
str_log = self.get_timestamp() + ' 本次备份失败 !'
print(str_log)
self.write_log(str_log)
# 推送钉钉消息
self.send_msg(2, '压缩文件失败!')
return
str_log = self.get_timestamp() + ' 压缩文件完成!'
print(str_log)
self.write_log(str_log)
# 上传文件
ret = self.upload_file()
if 0 != ret:
str_log = self.get_timestamp() + ' 本次备份失败!'
print(str_log)
self.write_log(str_log)
# 推送钉钉消息
self.send_msg(2, '上传文件失败!')
return
str_log = self.get_timestamp() + ' 上传文件完成!'
print(str_log)
self.write_log(str_log)
# 删除七牛云过期文件
self.delete_qny_invalid_file()
str_log = self.get_timestamp() + ' 删除七牛云过期文件完成!'
print(str_log)
self.write_log(str_log)
# 删除本地过期文件
self.delete_local_invalid_file()
str_log = self.get_timestamp() + ' 删除本地过期文件完成!'
print(str_log)
self.write_log(str_log)
# 推送钉钉消息
self.send_msg(1, '')
str_log = self.get_timestamp() + ' 本次备份成功!'
print(str_log)
self.write_log(str_log)
def replace_file_path(self):
if not self.src_path.endswith('/'):
self.src_path = self.src_path + '/'
if not self.dest_path.endswith('/'):
self.dest_path = self.dest_path + '/'
if not self.qny_dir.endswith('/'):
self.qny_dir = self.qny_dir + '/'
def get_now_date(self):
self.now_date = datetime.strptime(datetime.now().strftime("%Y.%m.%d %H:%M:%S"), "%Y.%m.%d %H:%M:%S")
@staticmethod
def get_timestamp() -> str:
return datetime.now().strftime('%Y-%m-%d %H:%M:%S')
def write_log(self, str_log: str):
with open(self.log_path, 'a', encoding='utf-8') as l:
str_log = str_log + '\n'
l.write(str_log)
def get_need_backup_dir_name(self) -> str:
if self.src_path is '':
return ''
str_src_file_name = ''
if not self.src_path.endswith('/'):
str_src_file_name = self.src_path[self.src_path.rfind('/') + 1:]
self.src_path = self.src_path + '/'
else:
str_src_file_name = self.src_path[:-1]
str_src_file_name = str_src_file_name[str_src_file_name.rfind('/') + 1:]
return str_src_file_name
def delete_local_invalid_file(self):
list_file_name = list()
for file in os.listdir(self.dest_path):
ret = re.compile(self.regex).findall(file)
if ret:
list_file_name.append(file)
if len(list_file_name) <= self.remain_num:
return
# 排序
list_file_name.sort()
need_delete_num = len(list_file_name) - self.remain_num
for index in range(0, need_delete_num):
str_file_path = self.dest_path + list_file_name[0]
list_file_name.pop(0)
os.remove(str_file_path)
str_log = self.get_timestamp() + ' 删除本地过期文件' + str_file_path
print(str_log)
self.write_log(str_log)
def delete_qny_invalid_file(self):
list_need_file = list()
list_file = self.get_file_list()
for file in list_file:
temp_file = file[len(self.qny_dir):]
ret = re.compile(self.regex).findall(temp_file)
if ret:
list_need_file.append(file)
if len(list_need_file) <= self.remain_num:
return
list_need_file.sort()
need_delete_num = len(list_need_file) - self.remain_num
for index in range(0, need_delete_num):
ret, info = self.bucket.delete(self.bucket_name, list_need_file[0])
str_log = self.get_timestamp() + ' 删除七牛云过期文件:' + list_need_file[0]
list_need_file.pop(0)
print(str_log)
self.write_log(str_log)
def get_file_list(self) -> list:
# 前缀
prefix = self.qny_dir # 填写存储桶的目标文件夹
# 列举条目
limit = 1000
# 列举出除'/'的所有文件以及以'/'为分隔的所有前缀
delimiter = None
# 标记
marker = None
ret, eof, info = self.bucket.list(self.bucket_name, prefix, marker, limit, delimiter)
items: list = ret.get('items')
# 所有百分号(%25)都被转义为了百分号(%), 将 25 补充回去
keys = []
for item in items:
key = item.get('key')
idx = key.find('%')
if idx >= 0:
key = f'{key[:idx + 1]}25{key[idx + 1:]}'
keys.append(key)
return keys
def compress_file(self) -> int:
# 拷贝文件夹
cmd = 'cp -R ' + self.src_path + ' ' + self.dest_path
ret = os.system(cmd)
if 0 != ret:
str_msg = self.get_timestamp()
str_msg += ' 复制文件夹失败!'
print(str_msg)
self.write_log(str_msg)
return 1
# 切换目录并压缩文件夹
self.compressed_file_name = self.file_name + '-' + datetime.now().date().strftime('%Y-%m-%d') + '.tar.gz'
cmd = 'cd ' + self.dest_path + '&& tar -zcvf ' + self.compressed_file_name + ' ' + self.dest_path + \
self.file_name + '/ --remove-file'
ret = os.system(cmd)
if 0 != ret:
str_msg = self.get_timestamp()
str_msg += ' 压缩文件夹失败!'
print(str_msg)
self.write_log(str_msg)
return 1
return 0
def upload_file(self) -> int:
# 上传后保存的文件全路径
file_path = self.qny_dir + self.compressed_file_name
# 生成上传token
token = self.q.upload_token(self.bucket_name, file_path)
# 要上传文件的路径
localfile = self.dest_path + self.compressed_file_name
ret, info = put_file(token, file_path, localfile)
if ret is not None:
if ret['hash'] != etag(localfile):
str_log = self.get_timestamp() + ' 文件上传失败!'
print(str_log)
self.write_log(str_log)
return 1
return 0
@staticmethod
def get_file_size(file_path: str) -> str:
size = os.path.getsize(file_path) / 1024 / 1024
size = '%.2f' % size
return size
def get_backup_file_list(self) -> str:
content = ''
for file in os.listdir(self.dest_path):
if file.find(self.file_name) == -1:
continue
content += file + ' ' + self.get_file_size(os.path.join(self.dest_path, file)) + ' MB'
content += '\n'
return content
def send_msg(self, backup_status: int, failed_reason: str):
header = {"Content-Type": "application/json;charset=utf-8"}
url = self.web_hook
# content里面要设置关键字
msg_content = ''
if backup_status == 1:
content = self.get_backup_file_list()
msg_content = self.get_timestamp() + ' 文件(或目录)备份成功,当前存在备份文件:\n' + content
data_info = {
"msgtype": "text",
"text": {
"content": msg_content
},
"isAtAll": False
}
value = json.dumps(data_info)
response = requests.post(url, data=value, headers=header)
err_msg = response.json()['errmsg']
if err_msg == 'ok':
self.write_log(msg_content[:-1] + ',钉钉消息推送成功。')
else:
msg_content = msg_content[:-1] + ',钉钉消息推送失败,失败原因:'
self.write_log(msg_content + err_msg)
elif backup_status == 2:
msg_content = self.get_timestamp() + ' 文件(或目录)备份失败,失败原因:' + failed_reason
data_info = {
"msgtype": "text",
"text": {
"content": msg_content
},
"isAtAll": False
}
value = json.dumps(data_info)
response = requests.post(url, data=value, headers=header)
err_msg = response.json()['errmsg']
if err_msg == 'ok':
self.write_log(msg_content + ',钉钉消息推送成功。')
else:
msg_content = msg_content + ',钉钉消息推送失败,失败原因:'
self.write_log(msg_content + err_msg)
if __name__ == '__main__':
backup = BackupSingleFile2Qiniuyun()
backup.start_backup()