一、引言
类似于Win7及以前的公文包,但是单向增量备份,不修改源文件。
Win10以后没有公文包了,吐槽一下,自己动手做一个!
二、GIF演示最终成果
三、完整代码
"""
程序说明:
一、时间:2021.8.17
二、作者:岳涛
三、功能:通过比较MD5值,实现源文件夹到目标文件夹的文件单向增量备份。类似Win7及以前的公文包,只是不会修改源文件夹。
四、开发相关:
4.1 windows操作DOS命令:xcopy 源文件夹\* 目标文件夹 /s /e /y
4.2 目前判定两个文件是否相同,除了按字节逐个对比这个笨方法外,简单常用的办法就是利用MD5和CRC校验,或是按一定规律挑取文件的指定位置的数据块就行对比。
这次利用文件的MD5值,将目标文件夹中已有文件的MD5值保存到字典的key中,每在源文件夹中读取一个文件就判定该文件的MD5值是否已经存在于MD5字典的key中,没有的话再进行复制操作,并将该文件的MD5值写入字典的key。
保存MD5值时,字典、元组和列表均可选用,检索速度从快到慢:元组>字典>列表。
但元组不能修改,添加MD5值时需要先转为列表,添加完再转为元组,反复转换编写代码繁琐且运行时也耗时,转换消耗的时间可能超过元组比字典快的那点时间。
所以本模块选用字典的key来存储MD5值,字典的key可以哈希,比列表的逐个遍历快很多。
4.3 GUI布局主要采用PySimpleGUI的sg.Column(),其第一个参数必须为列表等可迭代对象。
另外使用了sg.theme()、sg.Text()、sg.ProgressBar()、sg.Input()、sg.FolderBrowse()、sg.Button()、sg.Output()、sg.Window()等组件
4.4 为了使用方便,源文件夹和目标文件夹的默认值可以在程序同名配置文件中设置。
4.5 编译可执行文件:pyinstaller -F -w --icon=logo.ico 文件增量备份_GUI.py,可执行文件夹包括三个文件:文件增量备份_GUI.exe,文件增量备份_GUI.cfg,logo.ico
"""
import os
import sys
import hashlib
import shutil
import stat
import PySimpleGUI as sg
theme_dict = {'BACKGROUND': 'DeepSkyBlue4',
'TEXT': '#FFFFFF',
'INPUT': '#F2EFE8',
'TEXT_INPUT': '#000000',
'SCROLL': '#F2EFE8',
'BUTTON': ('#000000', '#C2D4D8'),
'PROGRESS': ('#FFFFFF', '#C7D5E0'),
'BORDER': 1,'SLIDER_DEPTH': 0, 'PROGRESS_DEPTH': 0}
sg.LOOK_AND_FEEL_TABLE['Dashboard'] = theme_dict
sg.theme('Dashboard')
BORDER_COLOR = '#C7D5E0'
DARK_HEADER_COLOR = 'orange'
BPAD_TOP = ((20,20), (20, 10))
BPAD_LEFT = ((20,10), (0, 10))
BPAD_LEFT_INSIDE = (0, 10)
BPAD_RIGHT = ((10,20), (10, 20))
top = [[sg.ProgressBar(1, orientation='h', size=(80, 50), key='progress',bar_color=('orange', 'white')),
sg.Text('0.0% ', key='percent', font='Any 16'),
sg.Text('', key='cur_num', font='Any 16'),
sg.Text('/', font='Any 16'),
sg.Text('', key='sum_num', font='Any 16'),
], ]
# 提取配置文件中的源文件与目标文件的初始值
with open(sys.argv[0].split('.')[0] + '.cfg', 'r', encoding='utf8') as f:
lines = f.readlines()
# print(len(lines))
for line in lines:
if line.split('=')[0].strip() == 'init_dir_src':
init_dir_src = line.split('=')[1].strip()
if line.split('=')[0].strip() == 'init_dir_tar':
init_dir_tar = line.split('=')[1].strip()
block_2 = [[sg.Text('源文件夹', font='Any 16'), sg.Text('(配置文件可修改默认值)', font='Any 8')],
[sg.Input(init_dir_src, key='folder_src'), sg.FolderBrowse(' 选择文件夹 ')],
[sg.Text('目标文件夹', font='Any 16'), sg.Text('(配置文件可修改默认值)', font='Any 8')],
[sg.Input(init_dir_tar, key='folder_tar'), sg.FolderBrowse(' 选择文件夹 ')],]
block_3 = [[sg.Button('开始备份', font='Any 20',pad=(60,50), key='start'),
sg.Button(' 退 出 ', font='Any 20', key= 'exit')],]
block_4 = [[sg.Text('当前进度', font='Any 20'), sg.Text('(MD5相同文件不显示)', font='Any 12')],
[sg.Output(size=(59, 16), font=('Helvetica 10'), background_color=BORDER_COLOR)],]
# sg.Column()第一个参数必须为列表等可迭代对象
layout = [[sg.Column(top, size=(920, 60), pad=BPAD_TOP)],
[sg.Column([[sg.Column(block_2, size=(450,150), pad=BPAD_LEFT_INSIDE)],
[sg.Column(block_3, size=(450,150), pad=BPAD_LEFT_INSIDE, background_color='DeepSkyBlue3')]],
pad=BPAD_LEFT, background_color=BORDER_COLOR),
sg.Column(block_4, size=(450, 320), pad=BPAD_RIGHT)]]
window = sg.Window('文件增量备份(根据MD5码,单向增量备份。)'+ ' '*58 + '--by 岳涛', layout, margins=(0,0),
background_color=BORDER_COLOR, no_titlebar=False, grab_anywhere=True, icon='logo.ico')
def create_tar_md5_dict(path):
""" 创建目标文件夹的md5值 """
global glo_j
for name in os.listdir(path):
# 更新进度条及百分比等
glo_j += 1
progress_bar.update_bar(glo_j, files_num)
window['percent'].update(str(round(100 * glo_j / files_num, 1)) + '% ')
window['cur_num'].update(str(glo_j))
window['sum_num'].update(str(files_num))
# python是跨平台的,os.sep为系统分隔符。在Windows上是'\',在Linux上os.sep是'/'
filename = path + os.sep + name
# 如果是文件夹,就递归调用本函数
if os.path.isdir(filename):
create_tar_md5_dict(filename)
# 如果是文件,生成文件的MD5码,以{key=md5值:value=1}tar_md5_dict,
else:
# 生成MD5值,对于MP4之类的大文件,耗时较长
with open(filename, 'rb') as f:
md5 = hashlib.md5(f.read()).hexdigest()
if md5 not in tar_md5_dict:
tar_md5_dict[md5] = 1
def copy_file(paths, patht):
""" 从源文件夹复制MD5不存在的文件到目标文件夹 """
global glo_j
for filename in os.listdir(paths):
# 更新进度条及百分比等
glo_j += 1
progress_bar.update_bar(glo_j, files_num)
window['percent'].update(str(round(100 * glo_j / files_num, 1)) + '% ')
window['cur_num'].update(str(glo_j))
window['sum_num'].update(str(files_num))
filename_s = paths + os.sep + filename
filename_t = patht + os.sep + filename
# 如果源为文件夹
if os.path.isdir(filename_s):
# 目标文件夹不存在,新建同名目标文件夹
if not os.path.exists(filename_t):
os.mkdir(filename_t)
# 递归调用本函数,复制源文件到目标文件
copy_file(filename_s, filename_t)
# 如果源为文件
else:
# 如果同名目标文件存在,且源文件和目标文件的修改日期和大小均一致,添加到已存在文件列表不复制
mdate_src= os.path.getmtime(filename_s)
size_src = os.path.getsize(filename_s)
if os.path.exists(filename_t) and os.path.getmtime(filename_t) == mdate_src and os.path.getsize(filename_t) == size_src:
exist_files.append(filename_t)
# 如果同名目标文件不存在,或源文件和目标文件的修改日期或大小不一致,比较MD5值
else:
# 生成源文件的MD5值
with open(filename_s, 'rb') as f_s:
data = f_s.read()
file_md5 = hashlib.md5(data).hexdigest()
# 如果源文件的MD5值不在tar_md5_dict中,添加该MD5到tar_md5_dict中,复制文件
if file_md5 not in tar_md5_dict:
tar_md5_dict[file_md5] = 1
print(f"复制:{filename_s}")
# 读取源文件的只读(可编辑)属性
Editable_src = os.access(filename_s, os.W_OK)
# 如果目标文件存在且具有只读属性,就移除它的只读属性,以便写入
if os.path.exists(filename_t) and os.access(filename_t, os.W_OK) == False:
os.chmod(filename_t, stat.S_IWRITE)
shutil.copyfile(filename_s, filename_t) # 这也是复制方法
# with open(filename_t, 'wb') as f_t:
# f_t.write(data)
# 如果源文件具有只读属性,设置目标文件的只读属性
if Editable_src == False:
os.system(f'attrib +R {filename_t}')
# 如果如果源文件的MD5值在tar_md5_dict中,添加到已存在文件列表不复制
else:
exist_files.append(filename_t)
def init_processbar(path):
""" 计算文件数量,初始化进度条初值 """
global files_num, glo_j
files_num = glo_j = 0
for root, dirs , files in os.walk(path):
files_num += len(files) # 累加文件数
files_num += len(dirs) # 累加目录数
if __name__ == '__main__':
# 界面循环
while True:
event, values = window.read() # 读取用户操作
if event == sg.WIN_CLOSED or event == 'exit': # 用户点击叉号或退出
break
if event == 'start': # 用户点击开始备份
progress_bar = window['progress']
path_src = window['folder_src'].get()
path_tar = window['folder_tar'].get()
tar_md5_dict = {}
exist_files = []
# 创建目标文件MD5
print('正在创建目标文件夹的MD5值,请稍候...', flush=True) # 输出到sg.Output()框中,不在终端输出
init_processbar(path_tar) # 计算目标文件数量,初始化进度条初值
create_tar_md5_dict(path_tar)
# 复制文件
print('正在增量备份文件,请稍候...', flush=True) # 输出到sg.Output()框中,不在终端输出
init_processbar(path_src) # 计算源文件数量,初始化进度条初值
copy_file(path_src, path_tar)
print('\n\n......备份完毕!')
window.close()
四、配套文件
4.1 配置文件
主要存储源文件夹和目标文件夹的默认值,方便不同人群使用
4.2 logo.ico文件
五、绿色可执行文件夹
该工具已编译为可执行文件,可脱离python环境,在Windows下独立运行。
pyinstaller -F -w --icon=logo.ico 文件增量备份_GUI.py
编译后的文件如下:
exe和cfg文件名可以随便修改,但要相同,logo.ico文件名不可以修改。