网盛资源- 最新刷机包,救砖解锁,刷机工具教程,免费下载,安卓免费APP下载,源码资源,破解软件资源

 找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 234|回复: 0

[电脑应用] 电脑自动备份插入的U盘数据

[复制链接]

1万

主题

1702

帖子

3万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
34614
发表于 2025-4-21 19:37:22 | 显示全部楼层 |阅读模式
https://wwvu.lanzouq.com/icus82tvlt9c
通过网盘分享的文件:电脑自动备份插入U盘数据.exe
链接: https://pan.baidu.com/s/1fI_QTf3F6DKw7cFzMJr2Hw?pwd=52pj 提取码: 52pj

  1. import os
  2. import shutil
  3. import time
  4. import string
  5. import win32file # 需要安装 pywin32
  6. import logging
  7. from datetime import datetime
  8. import threading
  9. from concurrent.futures import ThreadPoolExecutor, as_completed
  10. import tkinter as tk
  11. from tkinter import scrolledtext
  12. import queue

  13. # --- 配置 ---
  14. # 备份文件存储的基础路径 (请确保这个文件夹存在,或者脚本有权限创建它)
  15. BACKUP_DESTINATION_BASE = r"D:\USB_Backups"
  16. # 检测时间间隔(秒)
  17. CHECK_INTERVAL = 5
  18. # 日志文件路径
  19. LOG_FILE = os.path.join(BACKUP_DESTINATION_BASE, "usb_backup_log.txt")
  20. # --- 配置结束 ---

  21. # --- GUI 相关 ---
  22. class TextHandler(logging.Handler):
  23.     """自定义日志处理器,将日志记录发送到 Text 控件"""
  24.     def __init__(self, text_widget):
  25.         logging.Handler.__init__(self)
  26.         self.text_widget = text_widget
  27.         self.queue = queue.Queue()
  28.         # 启动**个线程来处理队列中的日志消息,避免阻塞主线程
  29.         self.thread = threading.Thread(target=self.process_queue, daemon=True)
  30.         self.thread.start()

  31.     def emit(self, record):
  32.         msg = self.format(record)
  33.         self.queue.put(msg)

  34.     def process_queue(self):
  35.         while True:
  36.             try:
  37.                 msg = self.queue.get()
  38.                 if msg is None: # Sentinel value to stop the thread
  39.                     break
  40.                 # Schedule GUI update on the main thread
  41.                 def update_widget():
  42.                     try:
  43.                         self.text_widget.configure(state='normal')
  44.                         self.text_widget.insert(tk.END, msg + '\n')
  45.                         self.text_widget.configure(state='disabled')
  46.                         self.text_widget.yview(tk.END)
  47.                     except tk.TclError: # Handle cases where the widget might be destroyed
  48.                         pass
  49.                 self.text_widget.after(0, update_widget)
  50.                 self.queue.task_done()
  51.             except Exception:
  52.                 # 处理可能的窗口已销毁等异常
  53.                 import traceback
  54.                 traceback.print_exc()
  55.                 break

  56.     def close(self):
  57.         self.stop_processing() # Signal the thread to stop
  58.         # Don't join here to avoid blocking the main thread
  59.         logging.Handler.close(self)

  60.     def stop_processing(self):
  61.         """Signals the processing thread to stop without waiting for it."""
  62.         self.queue.put(None) # Send sentinel to stop the processing thread

  63. class App(tk.Tk):
  64.     def __init__(self):
  65.         super().__init__()
  66.         self.title("USB 自动备份")
  67.         self.geometry("600x400")

  68.         self.log_text = scrolledtext.ScrolledText(self, state='disabled', wrap=tk.WORD)
  69.         self.log_text.pack(expand=True, fill='both', padx=10, pady=5)

  70.         self.status_label = tk.Label(self, text="状态: 初始化中...", anchor='w')
  71.         self.status_label.pack(fill='x', padx=10, pady=2)

  72.         self.exit_button = tk.Button(self, text="退出", command=self.quit_app)
  73.         self.exit_button.pack(pady=5)

  74.         self.backup_thread = None
  75.         self.running = True
  76.         self.protocol("WM_DELETE_WINDOW", self.quit_app)

  77.         self.configure_logging()

  78.     def configure_logging(self):
  79.         # 日志配置前先确保备份目录存在
  80.         if not os.path.exists(BACKUP_DESTINATION_BASE):
  81.             try:
  82.                 os.makedirs(BACKUP_DESTINATION_BASE)
  83.             except Exception as e:
  84.                 # 如果无法创建目录,在GUI中显示错误
  85.                 self.update_status(f"错误: 无法创建备份目录 {BACKUP_DESTINATION_BASE}: {e}")
  86.                 self.log_text.configure(state='normal')
  87.                 self.log_text.insert(tk.END, f"错误: 无法创建备份目录 {BACKUP_DESTINATION_BASE}: {e}\n")
  88.                 self.log_text.configure(state='disabled')
  89.                 return

  90.         log_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')

  91.         # 文件处理器
  92.         file_handler = logging.FileHandler(LOG_FILE)
  93.         file_handler.setFormatter(log_formatter)

  94.         # GUI 文本框处理器
  95.         self.text_handler = TextHandler(self.log_text)
  96.         self.text_handler.setFormatter(log_formatter)

  97.         # 获取根 logger 并添加处理器
  98.         root_logger = logging.getLogger()
  99.         root_logger.setLevel(logging.INFO)
  100.         # 清除可能存在的默认处理器(例如 basicConfig 创建的 StreamHandler)
  101.         if root_logger.hasHandlers():
  102.             root_logger.handlers.clear()
  103.         root_logger.addHandler(file_handler)
  104.         root_logger.addHandler(self.text_handler)
  105.         # 添加**个 StreamHandler 以便在控制台也看到日志(调试用)
  106.         # stream_handler = logging.StreamHandler()
  107.         # stream_handler.setFormatter(log_formatter)
  108.         # root_logger.addHandler(stream_handler)

  109.     def update_status(self, message):
  110.         # 使用 self.after 将 GUI 更新调度回主线程
  111.         self.after(0, lambda: self.status_label.config(text=f"状态: {message}"))

  112.     def start_backup_monitor(self):
  113.         self.backup_thread = threading.Thread(target=run_backup_monitor, args=(self,), daemon=True)
  114.         self.backup_thread.start()

  115.     def quit_app(self):
  116.         logging.info("收到退出信号,程序即将关闭。")
  117.         self.running = False # Signal the backup thread to stop

  118.         # Signal the logger thread to stop processing new messages
  119.         if hasattr(self, 'text_handler'):
  120.             self.text_handler.stop_processing()

  121.         # Give the backup thread a short time to finish
  122.         if self.backup_thread and self.backup_thread.is_alive():
  123.             try:
  124.                 self.backup_thread.join(timeout=1.0) # Wait max 1 second
  125.                 if self.backup_thread.is_alive():
  126.                     logging.warning("备份线程未能在1秒内停止,将强制关闭窗口。")
  127.             except Exception as e:
  128.                 logging.error(f"等待备份线程时出错: {e}")

  129.         # Close the main window. Daemon threads will be terminated.
  130.         self.destroy()
  131.         # os._exit(0) # Avoid force exit, let the application close naturally

  132. # --- 核心备份逻辑 (从旧 main 函数提取) ---
  133. def get_available_drives():
  134.     """获取当前所有可用的驱动器盘符"""
  135.     drives = []
  136.     bitmask = win32file.GetLogicalDrives()
  137.     for letter in string.ascii_uppercase:
  138.         if bitmask & 1:
  139.             drives.append(letter)
  140.         bitmask >>= 1
  141.     return set(drives)

  142. def is_removable_drive(drive_letter):
  143.     """判断指定盘符是否是可移动驱动器 (U盘通常是这个类型)"""
  144.     drive_path = f"{drive_letter}:"
  145.     try:
  146.         # DRIVE_REMOVABLE 的类型代码是 2
  147.         return win32file.GetDriveTypeW(drive_path) == win32file.DRIVE_REMOVABLE
  148.     except Exception as e:
  149.         # logging.error(f"检查驱动器 {drive_path} 类型时出错: {e}") # 可能在驱动器刚插入时发生
  150.         return False

  151. def should_skip_file(src, dst):
  152.     """判断是否需要跳过备份(增量备份逻辑)"""
  153.     if not os.path.exists(dst):
  154.         return False
  155.     try:
  156.         src_stat = os.stat(src)
  157.         dst_stat = os.stat(dst)
  158.         # 文件大小和修改时间都相同则跳过
  159.         return src_stat.st_size == dst_stat.st_size and int(src_stat.st_mtime) == int(dst_stat.st_mtime)
  160.     except Exception:
  161.         return False

  162. def copy_file_with_log(src, dst):
  163.     try:
  164.         file_size = os.path.getsize(src)
  165.         # 超过128MB的大文件采用分块复制
  166.         if file_size > 128 * 1024 * 1024:
  167.             chunk_size = 16 * 1024 * 1024  # 16MB
  168.             with open(src, 'rb') as fsrc, open(dst, 'wb') as fdst:
  169.                 while True:
  170.                     chunk = fsrc.read(chunk_size)
  171.                     if not chunk:
  172.                         break
  173.                     fdst.write(chunk)
  174.             # 尝试复制亓数据,如果失败则记录但不中断
  175.             try:
  176.                 shutil.copystat(src, dst)
  177.             except Exception as e_stat:
  178.                 logging.warning(f"无法复制亓数据 {src} -> {dst}: {e_stat}")
  179.             logging.info(f"分块复制大文件: {src} -> {dst}")
  180.         else:
  181.             shutil.copy2(src, dst)
  182.             logging.info(f"已复制: {src} -> {dst}")
  183.     except Exception as e:
  184.         logging.error(f"复制文件 {src} 时出错: {e}")

  185. def threaded_copytree(src, dst, skip_exts=None, skip_dirs=None, max_workers=8):
  186.     """线程池递归复制目录,支持增量备份和跳过指定类型,限制**大线程数"""
  187.     if skip_exts is None:
  188.         skip_exts = ['.tmp', '.log', '.sys']
  189.     if skip_dirs is None:
  190.         skip_dirs = ['$RECYCLE.BIN', 'System Volume Information']
  191.     if not os.path.exists(dst):
  192.         try:
  193.             os.makedirs(dst)
  194.         except Exception as e_mkdir:
  195.             logging.error(f"创建目录 {dst} 失败: {e_mkdir}")
  196.             return #无法创建目标目录,则无法继续复制
  197.     tasks = []
  198.     small_files = []
  199.     try:
  200.         with ThreadPoolExecutor(max_workers=max_workers) as executor:
  201.             for item in os.listdir(src):
  202.                 s = os.path.join(src, item)
  203.                 d = os.path.join(dst, item)
  204.                 try:
  205.                     if os.path.isdir(s):
  206.                         if item in skip_dirs:
  207.                             logging.info(f"跳过目录: {s}")
  208.                             continue
  209.                         # 递归调用也放入线程池
  210.                         tasks.append(executor.submit(threaded_copytree, s, d, skip_exts, skip_dirs, max_workers))
  211.                     else:
  212.                         ext = os.path.splitext(item)[1].lower()
  213.                         if ext in skip_exts:
  214.                             logging.info(f"跳过文件: {s}")
  215.                             continue
  216.                         if should_skip_file(s, d):
  217.                             # logging.debug(f"跳过未变更文件: {s}") # 改为 debug 级别
  218.                             continue
  219.                         # 小于16MB的小文件批量处理
  220.                         if os.path.getsize(s) < 16 * 1024 * 1024:
  221.                             small_files.append((s, d))
  222.                         else:
  223.                             tasks.append(executor.submit(copy_file_with_log, s, d))
  224.                 except PermissionError:
  225.                     logging.warning(f"无权限访问: {s},跳过")
  226.                 except FileNotFoundError:
  227.                     logging.warning(f"文件或目录不存在(可能在扫描时被移除): {s},跳过")
  228.                 except Exception as e_item:
  229.                     logging.error(f"处理 {s} 时出错: {e_item}")

  230.             # 批量提交小文件任务,减少线程调度开销
  231.             batch_size = 16
  232.             for i in range(0, len(small_files), batch_size):
  233.                 batch = small_files[i:i+batch_size]
  234.                 tasks.append(executor.submit(batch_copy_files, batch))

  235.             # 等待所有任务完成
  236.             for future in as_completed(tasks):
  237.                 try:
  238.                     future.result() # 获取结果以暴露异常
  239.                 except Exception as e_future:
  240.                     logging.error(f"线程池任务出错: {e_future}")
  241.     except PermissionError:
  242.         logging.error(f"无权限访问源目录: {src}")
  243.     except FileNotFoundError:
  244.         logging.error(f"源目录不存在: {src}")
  245.     except Exception as e_pool:
  246.         logging.error(f"处理目录 {src} 时线程池出错: {e_pool}")

  247. def batch_copy_files(file_pairs):
  248.     for src, dst in file_pairs:
  249.         copy_file_with_log(src, dst)

  250. def backup_usb_drive(drive_letter, app_instance):
  251.     """执行U盘备份(多线程+增量),并更新GUI状态"""
  252.     source_drive = f"{drive_letter}:"
  253.     timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
  254.     destination_folder = os.path.join(BACKUP_DESTINATION_BASE, f"Backup_{drive_letter}_{timestamp}")

  255.     logging.info(f"检测到U盘: {source_drive}")
  256.     app_instance.update_status(f"检测到U盘: {drive_letter}:\\,准备备份...")
  257.     logging.info(f"开始备份到: {destination_folder}")
  258.     app_instance.update_status(f"开始备份 {drive_letter}:\\ 到 {destination_folder}")

  259.     start_time = time.time()
  260.     try:
  261.         threaded_copytree(source_drive, destination_folder, max_workers=16)
  262.         end_time = time.time()
  263.         duration = end_time - start_time
  264.         logging.info(f"成功完成备份: {source_drive} -> {destination_folder} (耗时: {duration:.2f} 秒)")
  265.         app_instance.update_status(f"备份完成: {drive_letter}:\\ (耗时: {duration:.2f} 秒)")
  266.     except FileNotFoundError:
  267.         logging.error(f"错误:源驱动器 {source_drive} 不存在或无法访问。")
  268.         app_instance.update_status(f"错误: 无法访问 {drive_letter}:")
  269.     except PermissionError:
  270.         logging.error(f"错误:没有权限读取 {source_drive} 或写入 {destination_folder}。请检查权限设置。")
  271.         app_instance.update_status(f"错误: 权限不足 {drive_letter}:\\ 或目标文件夹")
  272.     except Exception as e:
  273.         logging.error(f"备份U盘 {source_drive} 时发生未知错误: {e}")
  274.         app_instance.update_status(f"错误: 备份 {drive_letter}:\\ 时发生未知错误")
  275.     finally:
  276.         # 短暂显示完成/错误状态后,恢复到空闲状态
  277.         app_instance.after(5000, lambda: app_instance.update_status("空闲,等待U盘插入..."))

  278. def run_backup_monitor(app_instance):
  279.     """后台监控线程的主函数"""
  280.     logging.info("U盘自动备份程序启动...")
  281.     logging.info(f"备份将存储在: {BACKUP_DESTINATION_BASE}")
  282.     app_instance.update_status("启动成功,等待U盘插入...")

  283.     # 检查备份目录是否已成功创建(在 App 初始化时完成)
  284.     if not os.path.exists(BACKUP_DESTINATION_BASE):
  285.         logging.error(f"无法启动监控:备份目录 {BACKUP_DESTINATION_BASE} 不存在且无法创建。")
  286.         app_instance.update_status(f"错误: 备份目录不存在且无法创建")
  287.         return

  288.     try:
  289.         known_drives = get_available_drives()
  290.         logging.info(f"当前已知驱动器: {sorted(list(known_drives))}")
  291.     except Exception as e_init_drives:
  292.         logging.error(f"初始化获取驱动器列表失败: {e_init_drives}")
  293.         app_instance.update_status(f"错误: 获取驱动器列表失败")
  294.         known_drives = set()

  295.     while app_instance.running:
  296.         try:
  297.             app_instance.update_status("正在检测驱动器...")
  298.             current_drives = get_available_drives()
  299.             new_drives = current_drives - known_drives
  300.             removed_drives = known_drives - current_drives

  301.             if new_drives:
  302.                 logging.info(f"检测到新驱动器: {sorted(list(new_drives))}")
  303.                 for drive in new_drives:
  304.                     if not app_instance.running: break # Check flag before potentially long operation
  305.                     # 稍作等待,确保驱动器已准备好
  306.                     logging.info(f"等待驱动器 {drive}: 准备就绪...")
  307.                     app_instance.update_status(f"检测到新驱动器 {drive}:,等待准备就绪...")
  308.                     time.sleep(3) # 增加等待时间
  309.                     if not app_instance.running: break
  310.                     try:
  311.                         if is_removable_drive(drive):
  312.                             backup_usb_drive(drive, app_instance)
  313.                         else:
  314.                             logging.info(f"驱动器 {drive}: 不是可移动驱动器,跳过备份。")
  315.                             app_instance.update_status(f"驱动器 {drive}: 非U盘,跳过")
  316.                             # 短暂显示后恢复空闲
  317.                             app_instance.after(3000, lambda: app_instance.update_status("空闲,等待U盘插入...") if app_instance.running else None)
  318.                     except Exception as e_check:
  319.                          logging.error(f"检查或备份驱动器 {drive}: 时出错: {e_check}")
  320.                          app_instance.update_status(f"错误: 处理驱动器 {drive}: 时出错")
  321.                          app_instance.after(5000, lambda: app_instance.update_status("空闲,等待U盘插入...") if app_instance.running else None)

  322.             if removed_drives:
  323.                 logging.info(f"检测到驱动器移除: {sorted(list(removed_drives))}")
  324.                 # Optionally update status for removed drives
  325.                 # app_instance.update_status(f"驱动器 {','.join(sorted(list(removed_drives)))} 已移除")
  326.                 # app_instance.after(3000, lambda: app_instance.update_status("空闲,等待U盘插入...") if app_instance.running else None)

  327.             # 更新已知驱动器列表
  328.             known_drives = current_drives

  329.             # 在循环末尾更新状态为空闲(如果没有正在进行的草作)
  330.             if not new_drives and app_instance.status_label.cget("text").startswith("状态: 正在检测驱动器"):
  331.                  app_instance.update_status("空闲,等待U盘插入...")

  332.             # 等待指定间隔,并允许提前退出
  333.             interval_counter = 0
  334.             while app_instance.running and interval_counter < CHECK_INTERVAL:
  335.                 time.sleep(1)
  336.                 interval_counter += 1
  337.             if not app_instance.running:
  338.                 break

  339.         except Exception as e:
  340.             logging.error(f"主循环发生错误: {e}")
  341.             app_instance.update_status(f"错误: {e}")
  342.             # 防止因临时错误导致程序崩溃,稍等后继续,并允许提前退出
  343.             error_wait_counter = 0
  344.             while app_instance.running and error_wait_counter < CHECK_INTERVAL * 2:
  345.                  time.sleep(1)
  346.                  error_wait_counter += 1
  347.             if not app_instance.running:
  348.                 break

  349.     logging.info("后台监控线程已停止。")
  350.     app_instance.update_status("程序已停止")

  351. if __name__ == "__main__":
  352.     app = App()
  353.     app.start_backup_monitor()
  354.     app.mainloop()
复制代码


回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|小黑屋|网盛资源论坛 ( 粤ICP备2022085098号 )|网站地图

GMT+8, 2025-9-2 04:35 , Processed in 0.318713 second(s), 23 queries .

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表