【Tools】文件增量备份 - 绿色可执行文件

一、引言

类似于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文件名不可以修改。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

岳涛@心馨电脑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值