├── requirements.txt ├── vmware-tools-12-5-0.pdf ├── vmware-tools-管理指南-12-5-0-CN.pdf ├── .gitattributes ├── LICENSE ├── README_CN.md ├── README.md ├── .gitignore └── sync_broadcom_tools.py /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | beautifulsoup4 3 | -------------------------------------------------------------------------------- /vmware-tools-12-5-0.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1564307973/vmware-tools-broadcom/HEAD/vmware-tools-12-5-0.pdf -------------------------------------------------------------------------------- /vmware-tools-管理指南-12-5-0-CN.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1564307973/vmware-tools-broadcom/HEAD/vmware-tools-管理指南-12-5-0-CN.pdf -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.iso filter=lfs diff=lfs merge=lfs -text 2 | *.zip filter=lfs diff=lfs merge=lfs -text 3 | *.exe filter=lfs diff=lfs merge=lfs -text 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Qinpengpeng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # VMware Tools 同步工具更新说明 2 | 3 | ## 🔍 官方目录结构详解 4 | 5 | 根据最新信息,`https://packages-prod.broadcom.com/tools/` 目录结构已更新,以下是关键目录说明: 6 | 7 | ### 📂 核心目录结构 8 | ``` 9 | 📁 tools/ 10 | ├── 📁 docs/ # 文档资源 11 | ├── 📁 esx/ # ESXi 相关工具 12 | ├── 📁 frozen/ # 遗留 VMware Tools(历史版本) 13 | ├── 📁 releases/ # 正式发布版本(重点目录) 14 | └── 📁 ... # 其他辅助目录 15 | ``` 16 | 17 | ### 🚀 最新版 VMware Tools 位置 18 | `releases/latest/` 目录包含最新版 VMware Tools(当前为 v13.0.0) 19 | 20 | ``` 21 | 📁 releases/latest/ 22 | ├── 📁 windows/ # Windows 平台工具 23 | │ ├── 📁 x64/ # 64位安装程序 24 | │ └── VMware-tools-windows-13.0.0-24696409.iso 25 | ├── 📁 linux/ # Linux 平台工具 26 | ├── 📁 macos/ # macOS 平台工具 27 | ├── 📁 repos/ # 仓库文件 28 | └── 📁 ubuntu/ # Ubuntu 专用包 29 | ``` 30 | 31 | ### ✅ Windows 平台文件示例 32 | | 文件类型 | 路径 | 大小 | 33 | |----------|------|------| 34 | | ISO 镜像 | `releases/latest/windows/VMware-tools-windows-13.0.0-24696409.iso` | 112MB | 35 | | 安装程序 | `releases/latest/windows/x64/VMware-tools-13.0.0-24696409-x64.exe` | 111MB | 36 | 37 | ### ⏳ 历史版本目录 38 | `releases/` 目录包含从 v10.x 到 v13.0.0 的所有历史版本: 39 | ``` 40 | 📁 releases/ 41 | ├── 📁 v10.0.0/ 42 | ├── 📁 v10.1.0/ 43 | ├── ... 44 | ├── 📁 v12.0.0/ 45 | ├── 📁 v12.5.0/ 46 | └── 📁 v13.0.0/ 47 | ``` 48 | 49 | ### ❄️ 遗留工具目录 (frozen) 50 | 包含旧平台支持文件: 51 | ``` 52 | 📁 frozen/ 53 | ├── 📁 darwin/ # macOS 旧版工具 54 | ├── 📁 linux/ # Linux 旧版工具 55 | ├── 📁 solaris/ # Solaris 工具 56 | └── 📁 windows/ # Windows 旧版工具 57 | └── winPreVista.iso # Windows Vista 之前版本 58 | ``` 59 | 60 | ## 🆕 最新版本信息 61 | - **版本号**: 13.0.0 62 | - **构建号**: 24696409 63 | - **发布日期**: 2025年6月18日 64 | - **支持平台**: 65 | - Windows (x86/x64) 66 | - Linux (各发行版) 67 | - macOS 68 | - Solaris 69 | - FreeBSD 70 | 71 | ## 💡 使用建议 72 | 73 | ### 1. 获取最新版本 74 | ```bash 75 | # 同步整个目录(包含所有历史版本) 76 | python sync_broadcom_tools.py 77 | 78 | # 本地查看最新版 79 | ls "VMware Tools/tools/releases/latest" 80 | ``` 81 | 82 | ### 2. 直接下载最新版(不运行脚本) 83 | - **Windows ISO**: 84 | [https://packages-prod.broadcom.com/tools/releases/latest/windows/VMware-tools-windows-13.0.0-24696409.iso](https://packages-prod.broadcom.com/tools/releases/latest/windows/VMware-tools-windows-13.0.0-24696409.iso) 85 | 86 | - **Linux 仓库**: 87 | [https://packages-prod.broadcom.com/tools/releases/latest/linux/](https://packages-prod.broadcom.com/tools/releases/latest/linux/) 88 | 89 | ### 3. 特殊需求 90 | - **历史版本**: 访问 `releases/v[版本号]/` 目录 91 | 示例: [https://packages-prod.broadcom.com/tools/releases/v12.5.0/](https://packages-prod.broadcom.com/tools/releases/v12.5.0/) 92 | 93 | - **遗留系统支持**: 访问 `frozen/` 目录 94 | 示例: [https://packages-prod.broadcom.com/tools/frozen/windows/winPreVista.iso](https://packages-prod.broadcom.com/tools/frozen/windows/winPreVista.iso) 95 | 96 | ## 🔄 脚本更新说明 97 | 当前脚本已支持同步最新目录结构,无需修改即可获取: 98 | 1. 最新版 `releases/latest/` 99 | 2. 历史版本 `releases/vXX.X.X/` 100 | 3. 遗留工具 `frozen/` 101 | 102 | ```bash 103 | # 同步后本地目录结构 104 | 📁 VMware Tools/ 105 | └── 📁 tools/ 106 | ├── 📁 docs/ 107 | ├── 📁 esx/ 108 | ├── 📁 frozen/ 109 | └── 📁 releases/ 110 | ├── 📁 latest/ 111 | ├── 📁 v10.0.0/ 112 | ├── ... 113 | └── 📁 v13.0.0/ 114 | ``` 115 | 116 | > **提示**:完整同步需要约 50GB 空间,若只需最新版,可手动下载 `releases/latest/` 内容 117 | 118 | > **温馨提示**:本工具仅用于技术交流,请遵守Broadcom官方使用条款 119 | 120 | ![Win95截图](https://cdn-dynmedia-1.microsoft.com/is/image/microsoftcorp/WIP_win95_1280x720?scl=1&fmt=png-alpha) 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Language Options 2 | 3 | - [中文](README_CN.md) 4 | - English 5 | 6 | --- 7 | 8 | # VMware Tools Sync Utility Update Notes 9 | 10 | ## 🔍 Official Directory Structure Explained 11 | 12 | According to the latest information, the structure under `https://packages-prod.broadcom.com/tools/` has been updated. Key directories are as follows: 13 | 14 | ### 📂 Core Directory Structure 15 | ``` 16 | 📁 tools/ 17 | ├── 📁 docs/ # Documentation resources 18 | ├── 📁 esx/ # ESXi related tools 19 | ├── 📁 frozen/ # Legacy VMware Tools (historical versions) 20 | ├── 📁 releases/ # Official releases (main directory) 21 | └── 📁 ... # Other auxiliary directories 22 | ``` 23 | 24 | ### 🚀 Location of the Latest VMware Tools 25 | The `releases/latest/` directory contains the most recent VMware Tools (currently v13.0.0): 26 | 27 | ``` 28 | 📁 releases/latest/ 29 | ├── 📁 windows/ # Windows tools 30 | │ ├── 📁 x64/ # 64-bit installers 31 | │ └── VMware-tools-windows-13.0.0-24696409.iso 32 | ├── 📁 linux/ # Linux tools 33 | ├── 📁 macos/ # macOS tools 34 | ├── 📁 repos/ # Repository files 35 | └── 📁 ubuntu/ # Ubuntu-specific packages 36 | ``` 37 | 38 | ### ✅ Example Windows Files 39 | | File Type | Path | Size | 40 | |-----------|------|------| 41 | | ISO Image | `releases/latest/windows/VMware-tools-windows-13.0.0-24696409.iso` | 112MB | 42 | | Installer | `releases/latest/windows/x64/VMware-tools-13.0.0-24696409-x64.exe` | 111MB | 43 | 44 | ### ⏳ Historical Versions Directory 45 | The `releases/` directory contains all historical versions from v10.x to v13.0.0: 46 | ``` 47 | 📁 releases/ 48 | ├── 📁 v10.0.0/ 49 | ├── 📁 v10.1.0/ 50 | ├── ... 51 | ├── 📁 v12.0.0/ 52 | ├── 📁 v12.5.0/ 53 | └── 📁 v13.0.0/ 54 | ``` 55 | 56 | ### ❄️ Legacy Tools Directory (frozen) 57 | Contains files for older platforms: 58 | ``` 59 | 📁 frozen/ 60 | ├── 📁 darwin/ # Old macOS tools 61 | ├── 📁 linux/ # Old Linux tools 62 | ├── 📁 solaris/ # Solaris tools 63 | └── 📁 windows/ # Old Windows tools 64 | └── winPreVista.iso # For Windows versions before Vista 65 | ``` 66 | 67 | ## 🆕 Latest Version Info 68 | - **Version**: 13.0.0 69 | - **Build Number**: 24696409 70 | - **Release Date**: June 18, 2025 71 | - **Supported Platforms**: 72 | - Windows (x86/x64) 73 | - Linux (various distributions) 74 | - macOS 75 | - Solaris 76 | - FreeBSD 77 | 78 | ## 💡 Usage Suggestions 79 | 80 | ### 1. Get the Latest Version 81 | ```bash 82 | # Sync the full directory (including all historical versions) 83 | python sync_broadcom_tools.py 84 | 85 | # Check the latest version locally 86 | ls "VMware Tools/tools/releases/latest" 87 | ``` 88 | 89 | ### 2. Download the Latest Version Directly (without script) 90 | - **Windows ISO**: 91 | [https://packages-prod.broadcom.com/tools/releases/latest/windows/VMware-tools-windows-13.0.0-24696409.iso](https://packages-prod.broadcom.com/tools/releases/latest/windows/VMware-tools-windows-13.0.0-24696409.iso) 92 | 93 | - **Linux Repository**: 94 | [https://packages-prod.broadcom.com/tools/releases/latest/linux/](https://packages-prod.broadcom.com/tools/releases/latest/linux/) 95 | 96 | ### 3. Special Requirements 97 | - **Historical Versions**: Visit the `releases/v[version]/` directory 98 | Example: [https://packages-prod.broadcom.com/tools/releases/v12.5.0/](https://packages-prod.broadcom.com/tools/releases/v12.5.0/) 99 | 100 | - **Legacy System Support**: Visit the `frozen/` directory 101 | Example: [https://packages-prod.broadcom.com/tools/frozen/windows/winPreVista.iso](https://packages-prod.broadcom.com/tools/frozen/windows/winPreVista.iso) 102 | 103 | ## 🔄 Script Update Notes 104 | The current script supports syncing the latest directory structure, no modification needed to fetch: 105 | 1. Latest release `releases/latest/` 106 | 2. Historical versions `releases/vXX.X.X/` 107 | 3. Legacy tools `frozen/` 108 | 109 | ```bash 110 | # Local directory structure after sync 111 | 📁 VMware Tools/ 112 | └── 📁 tools/ 113 | ├── 📁 docs/ 114 | ├── 📁 esx/ 115 | ├── 📁 frozen/ 116 | └── 📁 releases/ 117 | ├── 📁 latest/ 118 | ├── 📁 v10.0.0/ 119 | ├── ... 120 | └── 📁 v13.0.0/ 121 | ``` 122 | 123 | > **Note**: A full sync requires about 50GB of space. If you only need the latest version, you can manually download content from `releases/latest/`. 124 | 125 | > **Disclaimer**: This tool is intended solely for technical exchange. Please comply with Broadcom’s official terms of use. 126 | 127 | ![Win95截图](https://cdn-dynmedia-1.microsoft.com/is/image/microsoftcorp/WIP_win95_1280x720?scl=1&fmt=png-alpha) 128 | 129 | 130 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | sync.log 133 | .venv 134 | env/ 135 | venv/ 136 | ENV/ 137 | env.bak/ 138 | venv.bak/ 139 | 140 | # Spyder project settings 141 | .spyderproject 142 | .spyproject 143 | 144 | # Rope project settings 145 | .ropeproject 146 | 147 | # mkdocs documentation 148 | /site 149 | 150 | # mypy 151 | .mypy_cache/ 152 | .dmypy.json 153 | dmypy.json 154 | 155 | # Pyre type checker 156 | .pyre/ 157 | 158 | # pytype static type analyzer 159 | .pytype/ 160 | 161 | # Cython debug symbols 162 | cython_debug/ 163 | 164 | # PyCharm 165 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 166 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 167 | # and can be added to the global gitignore or merged into this file. For a more nuclear 168 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 169 | #.idea/ 170 | 171 | # Abstra 172 | # Abstra is an AI-powered process automation framework. 173 | # Ignore directories containing user credentials, local state, and settings. 174 | # Learn more at https://abstra.io/docs 175 | .abstra/ 176 | 177 | # Visual Studio Code 178 | # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore 179 | # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore 180 | # and can be added to the global gitignore or merged into this file. However, if you prefer, 181 | # you could uncomment the following to ignore the enitre vscode folder 182 | # .vscode/ 183 | 184 | # Ruff stuff: 185 | .ruff_cache/ 186 | 187 | # PyPI configuration file 188 | .pypirc 189 | 190 | # Cursor 191 | # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to 192 | # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data 193 | # refer to https://docs.cursor.com/context/ignore-files 194 | .cursorignore 195 | .cursorindexingignore 196 | -------------------------------------------------------------------------------- /sync_broadcom_tools.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | import time 4 | from urllib.parse import urljoin, urlparse 5 | from bs4 import BeautifulSoup 6 | import hashlib 7 | import logging 8 | import sys 9 | import datetime 10 | import concurrent.futures 11 | import re 12 | from pathlib import Path 13 | import argparse 14 | import platform 15 | 16 | # 配置 17 | BASE_URL = "https://packages-prod.broadcom.com/tools/" 18 | DEFAULT_LOCAL_ROOT = os.path.join("VMware Tools", "tools") 19 | HEADERS = { 20 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36", 21 | "Accept-Language": "en-US,en;q=0.5" 22 | } 23 | MAX_RETRIES = 3 24 | RETRY_DELAY = 5 # 秒 25 | DEFAULT_LOG_FILE = "vmware_tools_sync.log" 26 | 27 | # 设置日志系统 28 | def setup_logger(log_file): 29 | logger = logging.getLogger('VMwareToolsSync') 30 | logger.setLevel(logging.INFO) 31 | 32 | # 文件处理器 33 | file_handler = logging.FileHandler(log_file, encoding='utf-8') 34 | file_handler.setLevel(logging.INFO) 35 | 36 | # 控制台处理器 37 | console_handler = logging.StreamHandler(sys.stdout) 38 | console_handler.setLevel(logging.INFO) 39 | 40 | # 格式化 41 | formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S') 42 | file_handler.setFormatter(formatter) 43 | console_handler.setFormatter(formatter) 44 | 45 | logger.addHandler(file_handler) 46 | logger.addHandler(console_handler) 47 | 48 | return logger 49 | 50 | def get_remote_file_info(url, session): 51 | """获取远程文件信息""" 52 | file_info = {'size': 0, 'last_modified': None, 'etag': None} 53 | try: 54 | response = session.head(url, headers=HEADERS, timeout=15) 55 | response.raise_for_status() 56 | 57 | # 获取文件大小 58 | file_info['size'] = int(response.headers.get("Content-Length", 0)) 59 | 60 | # 获取最后修改时间 61 | last_modified = response.headers.get("Last-Modified") 62 | if last_modified: 63 | try: 64 | file_info['last_modified'] = time.mktime( 65 | datetime.datetime.strptime(last_modified, "%a, %d %b %Y %H:%M:%S %Z").timetuple()) 66 | except Exception: 67 | pass 68 | 69 | # 获取ETag 70 | file_info['etag'] = response.headers.get("ETag", "").strip('"') 71 | 72 | except requests.RequestException as e: 73 | logger.warning(f"⚠️ 无法获取远程文件信息: {url} - {e}") 74 | 75 | return file_info 76 | 77 | def should_download(url, local_path, remote_info, session): 78 | """检查是否需要下载文件""" 79 | # 新文件 80 | if not os.path.exists(local_path): 81 | logger.info(f" 新文件") 82 | return True 83 | 84 | try: 85 | # 获取本地文件信息 86 | local_size = os.path.getsize(local_path) 87 | local_mtime = os.path.getmtime(local_path) 88 | 89 | logger.info(f"🔍 检查文件: {os.path.basename(local_path)}") 90 | logger.info(f" 本地大小: {local_size} | 远程大小: {remote_info['size']}") 91 | 92 | # 大小检查 93 | if local_size != remote_info['size'] and remote_info['size'] > 0: 94 | logger.info(f" 文件大小变化: {local_size} → {remote_info['size']}") 95 | return True 96 | 97 | # 修改时间检查 98 | if remote_info['last_modified'] and remote_info['last_modified'] > local_mtime: 99 | logger.info(f" 远程文件更新: {time.ctime(local_mtime)} → {time.ctime(remote_info['last_modified'])}") 100 | return True 101 | 102 | # 哈希检查(如果ETag可用) 103 | if remote_info['etag']: 104 | local_hash = calculate_file_hash(local_path) 105 | if local_hash and remote_info['etag'] != local_hash: 106 | logger.info(f" 哈希值不匹配: 本地={local_hash[:8]}... 远程={remote_info['etag'][:8]}...") 107 | return True 108 | 109 | logger.info(f" 文件未变更") 110 | return False 111 | 112 | except Exception as e: 113 | logger.warning(f"⚠️ 文件检查出错: {url} - {e}") 114 | return True # 出错时下载以确保更新 115 | 116 | def calculate_file_hash(file_path): 117 | """计算文件的SHA-256哈希值""" 118 | sha256 = hashlib.sha256() 119 | try: 120 | with open(file_path, "rb") as f: 121 | while chunk := f.read(8192): 122 | sha256.update(chunk) 123 | return sha256.hexdigest() 124 | except Exception as e: 125 | logger.error(f"❌ 计算哈希值时出错 {file_path}: {e}") 126 | return None 127 | 128 | def download_file(task): 129 | """下载单个文件(线程安全)""" 130 | url, local_path, remote_info, session = task 131 | temp_path = f"{local_path}.tmp" 132 | 133 | try: 134 | logger.info(f"⬇️ 开始下载: {url}") 135 | start_time = time.time() 136 | 137 | with session.get(url, headers=HEADERS, stream=True, timeout=60) as r: 138 | r.raise_for_status() 139 | total_size = int(r.headers.get("Content-Length", remote_info['size'])) 140 | 141 | # 确保目录存在 142 | os.makedirs(os.path.dirname(local_path), exist_ok=True) 143 | 144 | with open(temp_path, "wb") as f: 145 | for chunk in r.iter_content(chunk_size=8192): 146 | if chunk: 147 | f.write(chunk) 148 | 149 | # 获取最后修改时间 150 | last_modified_header = r.headers.get('Last-Modified') 151 | 152 | # 验证下载 153 | downloaded_size = os.path.getsize(temp_path) 154 | if total_size > 0 and downloaded_size != total_size: 155 | raise IOError(f"大小不匹配: 预期 {total_size}, 实际 {downloaded_size}") 156 | 157 | # 替换旧文件 158 | if os.path.exists(local_path): 159 | os.remove(local_path) 160 | os.rename(temp_path, local_path) 161 | 162 | # 设置文件修改时间 163 | if last_modified_header: 164 | try: 165 | remote_time = time.mktime( 166 | datetime.datetime.strptime(last_modified_header, "%a, %d %b %Y %H:%M:%S %Z").timetuple()) 167 | os.utime(local_path, (remote_time, remote_time)) 168 | logger.debug(f" 文件修改时间已设置为: {last_modified_header}") 169 | except Exception as e: 170 | logger.warning(f"⚠️ 设置修改时间失败: {e}") 171 | 172 | # 记录统计信息 173 | dl_time = time.time() - start_time 174 | size_mb = downloaded_size / (1024 * 1024) 175 | speed = size_mb / dl_time if dl_time > 0 else 0 176 | logger.info(f"✅ 下载完成: {url}") 177 | logger.info(f" 大小: {size_mb:.2f} MB | 用时: {dl_time:.2f}秒 | 速度: {speed:.2f} MB/s") 178 | return True 179 | 180 | except Exception as e: 181 | logger.error(f"❌ 下载失败: {url}: {str(e)[:200]}") 182 | if os.path.exists(temp_path): 183 | try: 184 | os.remove(temp_path) 185 | except: 186 | pass 187 | return False 188 | 189 | def process_directory(url, local_dir, session): 190 | """处理目录及其内容(返回需要下载的文件列表)""" 191 | logger.info(f"\n📂 处理目录: {url}") 192 | download_tasks = [] 193 | 194 | try: 195 | response = session.get(url, headers=HEADERS, timeout=30) 196 | response.raise_for_status() 197 | soup = BeautifulSoup(response.text, 'html.parser') 198 | 199 | for link in soup.find_all('a'): 200 | href = link.get('href') 201 | if not href or href in ["../", "./"] or href.startswith(("?", "#", "javascript:")): 202 | continue 203 | 204 | full_url = urljoin(url, href) 205 | item_name = href.rstrip('/') 206 | 207 | # 处理目录 208 | if href.endswith('/'): 209 | sub_dir = os.path.join(local_dir, item_name) 210 | os.makedirs(sub_dir, exist_ok=True) 211 | logger.info(f" ├─ 进入子目录: {item_name}/") 212 | download_tasks.extend(process_directory(full_url, sub_dir, session)) 213 | # 处理文件 214 | else: 215 | local_path = os.path.join(local_dir, item_name) 216 | remote_info = get_remote_file_info(full_url, session) 217 | 218 | # 检查是否需要下载 219 | if should_download(full_url, local_path, remote_info, session): 220 | download_tasks.append((full_url, local_path, remote_info, session)) 221 | 222 | except Exception as e: 223 | logger.error(f"❌ 处理目录出错 {url}: {e}") 224 | 225 | return download_tasks 226 | 227 | def get_cpu_count(): 228 | """获取CPU核心数,用于确定线程数""" 229 | try: 230 | return os.cpu_count() or 4 231 | except: 232 | return 4 233 | 234 | def main(): 235 | # 解析命令行参数 236 | parser = argparse.ArgumentParser(description='VMware Tools 同步工具') 237 | parser.add_argument('--local-dir', type=str, default=DEFAULT_LOCAL_ROOT, 238 | help=f'本地存储目录 (默认: {DEFAULT_LOCAL_ROOT})') 239 | parser.add_argument('--log-file', type=str, default=DEFAULT_LOG_FILE, 240 | help=f'日志文件路径 (默认: {DEFAULT_LOG_FILE})') 241 | parser.add_argument('--threads', type=int, default=0, 242 | help='线程数 (默认: 根据CPU核心数自动设置)') 243 | parser.add_argument('--retries', type=int, default=MAX_RETRIES, 244 | help=f'重试次数 (默认: {MAX_RETRIES})') 245 | parser.add_argument('--delay', type=int, default=RETRY_DELAY, 246 | help=f'重试延迟 (秒) (默认: {RETRY_DELAY})') 247 | parser.add_argument('--full-sync', action='store_true', 248 | help='强制完全同步 (忽略本地文件)') 249 | 250 | args = parser.parse_args() 251 | 252 | # 设置全局logger 253 | global logger 254 | logger = setup_logger(args.log_file) 255 | 256 | # 显示系统信息 257 | logger.info(f"\n{'=' * 80}") 258 | logger.info(f"🚀 开始 VMware Tools 同步") 259 | logger.info(f"🖥️ 系统: {platform.system()} {platform.release()} ({platform.machine()})") 260 | logger.info(f"💻 CPU: {os.cpu_count()} 核心") 261 | logger.info(f"📁 本地目录: {os.path.abspath(args.local_dir)}") 262 | logger.info(f"📝 日志文件: {os.path.abspath(args.log_file)}") 263 | logger.info(f"⏰ 开始时间: {time.strftime('%Y-%m-%d %H:%M:%S')}") 264 | logger.info(f"{'=' * 80}\n") 265 | 266 | start_time = time.time() 267 | 268 | # 创建会话对象 269 | session = requests.Session() 270 | 271 | # 收集所有需要下载的文件 272 | all_tasks = process_directory(BASE_URL, args.local_dir, session) 273 | 274 | if args.full_sync: 275 | logger.info("🔁 强制完全同步模式 - 所有文件将被下载") 276 | 277 | # 准备下载任务 278 | download_tasks = [] 279 | for task in all_tasks: 280 | url, local_path, remote_info, session_ref = task 281 | if args.full_sync or should_download(url, local_path, remote_info, session): 282 | download_tasks.append((url, local_path, remote_info, session)) 283 | 284 | logger.info(f"📋 发现 {len(download_tasks)} 个文件需要下载") 285 | 286 | # 设置线程数 287 | thread_count = args.threads or min(get_cpu_count() * 2, 16) # 最多16线程 288 | logger.info(f"🧵 使用 {thread_count} 线程进行下载") 289 | 290 | # 多线程下载 291 | completed = 0 292 | failed = 0 293 | with concurrent.futures.ThreadPoolExecutor(max_workers=thread_count) as executor: 294 | # 提交所有任务 295 | future_to_task = {executor.submit(download_file, task): task for task in download_tasks} 296 | 297 | # 处理结果 298 | for future in concurrent.futures.as_completed(future_to_task): 299 | task = future_to_task[future] 300 | try: 301 | result = future.result() 302 | if result: 303 | completed += 1 304 | else: 305 | failed += 1 306 | except Exception as e: 307 | logger.error(f"❌ 任务执行出错: {e}") 308 | failed += 1 309 | 310 | # 统计结果 311 | duration = time.time() - start_time 312 | logger.info(f"\n{'=' * 80}") 313 | logger.info(f"✅ 同步完成! 用时: {duration:.2f} 秒") 314 | logger.info(f"📊 统计:") 315 | logger.info(f" 总文件数: {len(all_tasks)}") 316 | logger.info(f" 需要下载: {len(download_tasks)}") 317 | logger.info(f" 成功下载: {completed}") 318 | logger.info(f" 下载失败: {failed}") 319 | logger.info(f"⏰ 结束时间: {time.strftime('%Y-%m-%d %H:%M:%S')}") 320 | logger.info(f"{'=' * 80}") 321 | 322 | # 添加空行分隔每次运行 323 | with open(args.log_file, "a", encoding="utf-8") as log_file: 324 | log_file.write("\n\n") 325 | 326 | if __name__ == "__main__": 327 | main() 328 | --------------------------------------------------------------------------------