|
1 | 1 | import re |
2 | 2 | import sys |
| 3 | +import os |
| 4 | +import shutil |
| 5 | +import time |
| 6 | +import hashlib |
| 7 | +import datetime |
| 8 | +import json |
| 9 | +import threading |
| 10 | +import random |
| 11 | +from concurrent.futures import ThreadPoolExecutor, as_completed |
| 12 | + |
3 | 13 | try: |
4 | 14 | import plistlib |
5 | 15 | except ImportError: |
|
11 | 21 | import winreg |
12 | 22 | except ImportError: |
13 | 23 | winreg = None |
14 | | -import os, shutil, time, hashlib, datetime, json, threading, random |
| 24 | +else: |
| 25 | + winreg = None |
| 26 | + |
| 27 | +# Optional pywin32 |
15 | 28 | try: |
16 | | - import win32api, win32con |
17 | | -except ImportError: |
| 29 | + import win32api, win32con # type: ignore |
| 30 | +except Exception: |
18 | 31 | win32api = win32con = None |
| 32 | + |
19 | 33 | from PySide6.QtWidgets import ( |
20 | 34 | QMainWindow, QApplication, QPushButton, QWidget, QFileDialog, QMessageBox, |
21 | | - QGridLayout, QProgressDialog, QSpacerItem, QSizePolicy |
| 35 | + QGridLayout, QProgressDialog, QSpacerItem, QSizePolicy, QCheckBox, |
| 36 | + QDialog, QVBoxLayout, QListWidget, QAbstractItemView, QTableWidget, |
| 37 | + QTableWidgetItem, QLabel |
22 | 38 | ) |
23 | | -from PySide6.QtWidgets import QSizePolicy |
24 | 39 | from PySide6.QtCore import Qt, QThread, Signal, QTimer |
25 | 40 |
|
| 41 | +# ----------------------------------------------------------------------------- |
| 42 | +# Constants & helpers |
| 43 | +# ----------------------------------------------------------------------------- |
| 44 | +USER_ROOT = os.path.expandvars(r"C:\\Users") if sys.platform.startswith("win") else "/Users" |
| 45 | +TIMESTAMP_FMT = "%Y-%m-%d_%H-%M-%S" |
26 | 46 |
|
27 | 47 |
|
28 | | -def get_user_path(subpath=""): |
29 | | - """Return full path inside user's profile directory.""" |
30 | | - user_profile = os.environ.get("USERPROFILE", "") |
31 | | - return os.path.join(user_profile, subpath) if subpath else user_profile |
| 48 | +def ensure_logs_dir(root_dir: str) -> str: |
| 49 | + """Create and return a _logs directory inside root_dir.""" |
| 50 | + logs = os.path.join(root_dir, "_logs") |
| 51 | + os.makedirs(logs, exist_ok=True) |
| 52 | + return logs |
32 | 53 |
|
33 | 54 |
|
| 55 | +def new_log_file(root_dir: str, prefix: str) -> str: |
| 56 | + ts = datetime.datetime.now().strftime(TIMESTAMP_FMT) |
| 57 | + logs_dir = ensure_logs_dir(root_dir) |
| 58 | + return os.path.join(logs_dir, f"{prefix}_{ts}.log") |
| 59 | + |
| 60 | + |
| 61 | +def log_line(path: str, text: str) -> None: |
| 62 | + try: |
| 63 | + with open(path, "a", encoding="utf-8") as f: |
| 64 | + f.write(text.rstrip("\n") + "\n") |
| 65 | + except Exception: |
| 66 | + pass |
| 67 | + |
| 68 | + |
| 69 | +def chunked_file_hash(filepath: str, chunk_size: int = 1024 * 1024) -> str: |
| 70 | + hasher = hashlib.md5() |
| 71 | + with open(filepath, "rb") as f: |
| 72 | + while True: |
| 73 | + chunk = f.read(chunk_size) |
| 74 | + if not chunk: |
| 75 | + break |
| 76 | + hasher.update(chunk) |
| 77 | + return hasher.hexdigest() |
| 78 | + |
| 79 | + |
| 80 | +# ----------------------------------------------------------------------------- |
| 81 | +# Backup worker thread (multithreaded copy inside) |
| 82 | +# ----------------------------------------------------------------------------- |
34 | 83 | class BackupWorker(QThread): |
35 | | - progress_update = Signal(int, str, int, int, float) |
36 | | - finished = Signal(int, bool, str) |
| 84 | + progress_update = Signal(int, str, int, int, float) # files_processed, file, file_size, copied_size, speed |
| 85 | + finished = Signal(int, bool, str) # files_copied, success, message |
37 | 86 |
|
38 | | - def __init__(self, source_dir, destination_root, patterns): |
| 87 | + def __init__(self, source_dir: str, destination_root: str, patterns, log_file: str, max_workers: int | None = None): |
39 | 88 | super().__init__() |
40 | 89 | self.source_dir = source_dir |
41 | 90 | self.destination_root = destination_root |
|
0 commit comments