├── .gitignore ├── augutils ├── __init__.py ├── sqlite_modifier.py ├── json_modifier.py └── workspace_cleaner.py ├── utils ├── __init__.py ├── device_codes.py └── paths.py ├── LICENSE ├── index.py └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc -------------------------------------------------------------------------------- /augutils/__init__.py: -------------------------------------------------------------------------------- 1 | from .json_modifier import modify_telemetry_ids 2 | from .sqlite_modifier import clean_augment_data 3 | from .workspace_cleaner import clean_workspace_storage 4 | 5 | __all__ = ['modify_telemetry_ids', 'clean_augment_data', 'clean_workspace_storage'] -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .paths import get_home_dir, get_app_data_dir, get_storage_path, get_db_path, get_machine_id_path 2 | from .device_codes import generate_machine_id, generate_device_id 3 | 4 | __all__ = [ 5 | 'get_home_dir', 6 | 'get_app_data_dir', 7 | 'get_storage_path', 8 | 'get_db_path', 9 | 'get_machine_id_path', 10 | 'generate_machine_id', 11 | 'generate_device_id', 12 | 'get_workspace_storage_path' 13 | ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 vber 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 | -------------------------------------------------------------------------------- /utils/device_codes.py: -------------------------------------------------------------------------------- 1 | import os 2 | import uuid 3 | import secrets 4 | 5 | 6 | def generate_machine_id() -> str: 7 | """ 8 | Generate a random 64-character hex string for machine ID. 9 | Similar to using /dev/urandom in bash but using Python's cryptographic functions. 10 | 11 | Returns: 12 | str: A 64-character hexadecimal string 13 | """ 14 | # Generate 32 random bytes (which will become 64 hex characters) 15 | random_bytes = secrets.token_bytes(32) 16 | # Convert to hexadecimal string 17 | return random_bytes.hex() 18 | 19 | 20 | def generate_device_id() -> str: 21 | """ 22 | Generate a random UUID v4 for device ID. 23 | 24 | Returns: 25 | str: A lowercase UUID v4 string in the format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx 26 | where x is any hexadecimal digit and y is one of 8, 9, A, or B 27 | """ 28 | # Generate a random UUID v4 29 | device_id = str(uuid.uuid4()) 30 | return device_id.lower() 31 | 32 | 33 | def generate_sqm_id() -> str: 34 | """ 35 | Generate a random UUID for telemetry.sqmId in the format {XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX} (uppercase, curly braces). 36 | Returns: 37 | str: The generated sqmId string 38 | """ 39 | return '{' + str(uuid.uuid4()).upper() + '}' 40 | 41 | 42 | if __name__ == "__main__": 43 | # Example usage 44 | print(f"Machine ID: {generate_machine_id()}") 45 | print(f"Device ID: {generate_device_id()}") 46 | print(f"SQM ID: {generate_sqm_id()}") -------------------------------------------------------------------------------- /augutils/sqlite_modifier.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import shutil 3 | import time 4 | from utils.paths import get_db_path 5 | 6 | def _create_backup(file_path: str) -> str: 7 | """ 8 | Creates a backup of the specified file with timestamp. 9 | 10 | Args: 11 | file_path (str): Path to the file to backup 12 | 13 | Returns: 14 | str: Path to the backup file 15 | 16 | Format: .bak. 17 | """ 18 | timestamp = int(time.time()) 19 | backup_path = f"{file_path}.bak.{timestamp}" 20 | shutil.copy2(file_path, backup_path) 21 | return backup_path 22 | 23 | def clean_augment_data() -> dict: 24 | """ 25 | Cleans augment-related data from the SQLite database. 26 | Creates a backup before modification. 27 | 28 | This function: 29 | 1. Gets the SQLite database path 30 | 2. Creates a backup of the database file 31 | 3. Opens the database connection 32 | 4. Deletes records where key contains 'augment' 33 | 34 | Returns: 35 | dict: A dictionary containing operation results 36 | { 37 | 'db_backup_path': str, 38 | 'deleted_rows': int 39 | } 40 | """ 41 | db_path = get_db_path() 42 | 43 | # Create backup before modification 44 | db_backup_path = _create_backup(db_path) 45 | 46 | # Connect to the database 47 | conn = sqlite3.connect(db_path) 48 | cursor = conn.cursor() 49 | 50 | try: 51 | # Execute the delete query 52 | cursor.execute("DELETE FROM ItemTable WHERE key LIKE '%augment%'") 53 | deleted_rows = cursor.rowcount 54 | 55 | # Commit the changes 56 | conn.commit() 57 | 58 | return { 59 | 'db_backup_path': db_backup_path, 60 | 'deleted_rows': deleted_rows 61 | } 62 | finally: 63 | # Always close the connection 64 | cursor.close() 65 | conn.close() -------------------------------------------------------------------------------- /index.py: -------------------------------------------------------------------------------- 1 | from utils.paths import get_home_dir, get_app_data_dir, get_storage_path, get_db_path, get_machine_id_path,get_workspace_storage_path 2 | from augutils.json_modifier import modify_telemetry_ids 3 | from augutils.sqlite_modifier import clean_augment_data 4 | from augutils.workspace_cleaner import clean_workspace_storage 5 | 6 | if __name__ == "__main__": 7 | print("System Paths:") 8 | print(f"Home Directory: {get_home_dir()}") 9 | print(f"App Data Directory: {get_app_data_dir()}") 10 | print(f"Storage Path: {get_storage_path()}") 11 | print(f"DB Path: {get_db_path()}") 12 | print(f"Machine ID Path: {get_machine_id_path()}") 13 | print(f"Workspace Storage Path: {get_workspace_storage_path()}") 14 | 15 | print("\nModifying Telemetry IDs:") 16 | try: 17 | result = modify_telemetry_ids() 18 | print("\nBackup created at:") 19 | print(f"Storage backup path: {result['storage_backup_path']}") 20 | if result['machine_id_backup_path']: 21 | print(f"Machine ID backup path: {result['machine_id_backup_path']}") 22 | 23 | print("\nOld IDs:") 24 | print(f"Machine ID: {result['old_machine_id']}") 25 | print(f"Device ID: {result['old_device_id']}") 26 | 27 | print("\nNew IDs:") 28 | print(f"Machine ID: {result['new_machine_id']}") 29 | print(f"Device ID: {result['new_device_id']}") 30 | 31 | print("\nCleaning SQLite Database:") 32 | db_result = clean_augment_data() 33 | print(f"Database backup created at: {db_result['db_backup_path']}") 34 | print(f"Deleted {db_result['deleted_rows']} rows containing 'augment' in their keys") 35 | 36 | print("\nCleaning Workspace Storage:") 37 | ws_result = clean_workspace_storage() 38 | print(f"Workspace backup created at: {ws_result['backup_path']}") 39 | print(f"Deleted {ws_result['deleted_files_count']} files from workspace storage") 40 | 41 | print("Now you can run VS Code and login with the new email.") 42 | except FileNotFoundError as e: 43 | print(f"Error: {e}") -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Free AugmentCode 2 | 3 | [English](#english) | [中文](#chinese) 4 | 5 | # 中文版 6 | 7 | Free AugmentCode 是一个用于清理AugmentCode相关数据的工具,可以在同一台电脑上无限次登录不同的账号,避免账号被锁定。 8 | 9 | ## 功能特性 10 | 11 | - 📝 修改Telemetry ID 12 | - 重置设备 ID 和机器 ID 13 | - 自动备份原始数据 14 | - 生成新的随机 ID 15 | 16 | - 🗃️ 数据库清理 17 | - 清理 SQLite 数据库中的特定记录 18 | - 自动备份数据库文件 19 | - 删除包含 'augment' 关键字的记录 20 | 21 | - 💾 工作区存储管理 22 | - 清理工作区存储文件 23 | - 自动备份工作区数据 24 | 25 | ## 安装说明 26 | 27 | 1. 确保你的系统已安装 Python 3.10及以上 28 | 2. 克隆此仓库到本地: 29 | ```bash 30 | git clone https://github.com/yourusername/free-augmentcode.git 31 | cd free-augmentcode 32 | ``` 33 | 34 | ## 使用方法 35 | 36 | 1. 退出AugmentCode插件 37 | 2. 完全退出 VS Code 38 | 3. 执行脚本: 39 | 40 | ```bash 41 | python index.py 42 | ``` 43 | 44 | 4. 重新启动 VS Code 45 | 5. AugmentCode 插件中使用新的邮箱进行登录 46 | 47 | ## 项目结构 48 | 49 | ``` 50 | free-augmentcode/ 51 | ├── index.py # 主程序入口 52 | ├── augutils/ # 工具类目录 53 | │ ├── json_modifier.py # JSON 文件修改工具 54 | │ ├── sqlite_modifier.py # SQLite 数据库修改工具 55 | │ └── workspace_cleaner.py # 工作区清理工具 56 | └── utils/ # 通用工具目录 57 | └── paths.py # 路径管理工具 58 | ``` 59 | 60 | ## 贡献 61 | 62 | 欢迎提交 Issue 和 Pull Request 来帮助改进这个项目。 63 | 64 | ## 许可证 65 | 66 | 此项目采用 MIT 许可证。详见 [LICENSE](LICENSE) 文件。 67 | 68 | --- 69 | 70 | # English Version 71 | 72 | Free AugmentCode is a tool for cleaning AugmentCode-related data, allowing unlimited logins with different accounts on the same computer while avoiding account lockouts. 73 | 74 | ## Features 75 | 76 | - 📝 Telemetry ID Modification 77 | - Reset device ID and machine ID 78 | - Automatic backup of original data 79 | - Generate new random IDs 80 | 81 | - 🗃️ Database Cleanup 82 | - Clean specific records in SQLite database 83 | - Automatic database file backup 84 | - Remove records containing 'augment' keyword 85 | 86 | - 💾 Workspace Storage Management 87 | - Clean workspace storage files 88 | - Automatic workspace data backup 89 | 90 | ## Installation 91 | 92 | 1. Ensure Python 3.10 or above is installed on your system 93 | 2. Clone this repository: 94 | ```bash 95 | git clone https://github.com/yourusername/free-augmentcode.git 96 | cd free-augmentcode 97 | ``` 98 | 99 | ## Usage 100 | 101 | 1. Exit the AugmentCode plugin 102 | 2. Completely close VS Code 103 | 3. Run the script: 104 | 105 | ```bash 106 | python index.py 107 | ``` 108 | 109 | 4. Restart VS Code 110 | 5. Log in to the AugmentCode plugin with a new email 111 | 112 | ## Project Structure 113 | 114 | ``` 115 | free-augmentcode/ 116 | ├── index.py # Main program entry 117 | ├── augutils/ # Utility classes directory 118 | │ ├── json_modifier.py # JSON file modification tool 119 | │ ├── sqlite_modifier.py # SQLite database modification tool 120 | │ └── workspace_cleaner.py # Workspace cleanup tool 121 | └── utils/ # Common utilities directory 122 | └── paths.py # Path management tool 123 | ``` 124 | 125 | ## Contributing 126 | 127 | Issues and Pull Requests are welcome to help improve this project. 128 | 129 | ## License 130 | 131 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. -------------------------------------------------------------------------------- /augutils/json_modifier.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import time 4 | import shutil 5 | from utils.paths import get_storage_path, get_machine_id_path 6 | from utils.device_codes import generate_machine_id, generate_device_id, generate_sqm_id 7 | 8 | def _create_backup(file_path: str) -> str: 9 | """ 10 | Creates a backup of the specified file with timestamp. 11 | 12 | Args: 13 | file_path (str): Path to the file to backup 14 | 15 | Returns: 16 | str: Path to the backup file 17 | 18 | Format: .bak. 19 | """ 20 | timestamp = int(time.time()) 21 | backup_path = f"{file_path}.bak.{timestamp}" 22 | shutil.copy2(file_path, backup_path) 23 | return backup_path 24 | 25 | def modify_telemetry_ids() -> dict: 26 | """ 27 | Modifies the telemetry IDs in the VS Code storage.json file and machine ID file. 28 | Creates backups before modification. 29 | 30 | This function: 31 | 1. Creates backups of the storage.json and machine ID files 32 | 2. Reads the storage.json file 33 | 3. Generates new machine and device IDs 34 | 4. Updates the telemetry.machineId, telemetry.devDeviceId, and telemetry.sqmId values in storage.json 35 | 5. Updates the machine ID file with the new machine ID 36 | 6. Saves the modified files 37 | 38 | Returns: 39 | dict: A dictionary containing the old and new IDs and backup information 40 | { 41 | 'old_machine_id': str, 42 | 'new_machine_id': str, 43 | 'old_device_id': str, 44 | 'new_device_id': str, 45 | 'old_sqm_id': str, 46 | 'new_sqm_id': str, 47 | 'storage_backup_path': str, 48 | 'machine_id_backup_path': str 49 | } 50 | """ 51 | storage_path = get_storage_path() 52 | machine_id_path = get_machine_id_path() 53 | 54 | if not os.path.exists(storage_path): 55 | raise FileNotFoundError(f"Storage file not found at: {storage_path}") 56 | 57 | # Create backups before modification 58 | storage_backup_path = _create_backup(storage_path) 59 | machine_id_backup_path = None 60 | if os.path.exists(machine_id_path): 61 | machine_id_backup_path = _create_backup(machine_id_path) 62 | 63 | # Read the current JSON content 64 | with open(storage_path, 'r', encoding='utf-8') as f: 65 | data = json.load(f) 66 | 67 | # Store old values 68 | old_machine_id = data.get('telemetry.machineId', '') 69 | old_device_id = data.get('telemetry.devDeviceId', '') 70 | old_sqm_id = data.get('telemetry.sqmId', '') 71 | 72 | # Generate new IDs 73 | new_machine_id = generate_machine_id() 74 | new_device_id = generate_device_id() 75 | new_sqm_id = generate_sqm_id() 76 | 77 | # Update the values in storage.json 78 | data['telemetry.machineId'] = new_machine_id 79 | data['telemetry.devDeviceId'] = new_device_id 80 | data['telemetry.sqmId'] = new_sqm_id 81 | 82 | # Write the modified content back to storage.json 83 | with open(storage_path, 'w', encoding='utf-8') as f: 84 | json.dump(data, f, indent=4) 85 | 86 | # Write the new machine ID to the machine ID file 87 | with open(machine_id_path, 'w', encoding='utf-8') as f: 88 | f.write(new_device_id) 89 | 90 | return { 91 | 'old_machine_id': old_machine_id, 92 | 'new_machine_id': new_machine_id, 93 | 'old_device_id': old_device_id, 94 | 'new_device_id': new_device_id, 95 | 'old_sqm_id': old_sqm_id, 96 | 'new_sqm_id': new_sqm_id, 97 | 'storage_backup_path': storage_backup_path, 98 | 'machine_id_backup_path': machine_id_backup_path 99 | } -------------------------------------------------------------------------------- /utils/paths.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from pathlib import Path 4 | 5 | 6 | def get_home_dir() -> str: 7 | """ 8 | Get the user's home directory across different platforms. 9 | 10 | Returns: 11 | str: Path to the user's home directory 12 | """ 13 | return str(Path.home()) 14 | 15 | 16 | def get_app_data_dir() -> str: 17 | """ 18 | Get the application data directory across different platforms. 19 | 20 | Returns: 21 | str: Path to the application data directory 22 | 23 | Platform specific paths: 24 | - Windows: %APPDATA% (typically C:\\Users\\\\AppData\\Roaming) 25 | - macOS: ~/Library/Application Support 26 | - Linux: ~/.local/share 27 | """ 28 | if sys.platform == "win32": 29 | # Windows 30 | return os.getenv("APPDATA", "") 31 | elif sys.platform == "darwin": 32 | # macOS 33 | return os.path.join(str(Path.home()), "Library/Application Support") 34 | else: 35 | # Linux and other Unix-like systems 36 | return os.path.join(str(Path.home()), ".local/share") 37 | 38 | 39 | def get_storage_path() -> str: 40 | """ 41 | Get the storage.json path across different platforms. 42 | 43 | Returns: 44 | str: Path to the storage.json file 45 | 46 | Platform specific paths: 47 | - Windows: %APPDATA%/Code/User/globalStorage/storage.json 48 | - macOS: ~/Library/Application Support/Code/User/globalStorage/storage.json 49 | - Linux: ~/.config/Code/User/globalStorage/storage.json 50 | """ 51 | if sys.platform == "win32": 52 | # Windows 53 | base_path = os.getenv("APPDATA", "") 54 | return os.path.join(base_path, "Code", "User", "globalStorage", "storage.json") 55 | elif sys.platform == "darwin": 56 | # macOS 57 | return os.path.join(str(Path.home()), "Library", "Application Support", "Code", "User", "globalStorage", "storage.json") 58 | else: 59 | # Linux and other Unix-like systems 60 | return os.path.join(str(Path.home()), ".config", "Code", "User", "globalStorage", "storage.json") 61 | 62 | 63 | def get_db_path() -> str: 64 | """ 65 | Get the state.vscdb path across different platforms. 66 | 67 | Returns: 68 | str: Path to the state.vscdb file 69 | 70 | Platform specific paths: 71 | - Windows: %APPDATA%/Code/User/globalStorage/state.vscdb 72 | - macOS: ~/Library/Application Support/Code/User/globalStorage/state.vscdb 73 | - Linux: ~/.config/Code/User/globalStorage/state.vscdb 74 | """ 75 | if sys.platform == "win32": 76 | # Windows 77 | base_path = os.getenv("APPDATA", "") 78 | return os.path.join(base_path, "Code", "User", "globalStorage", "state.vscdb") 79 | elif sys.platform == "darwin": 80 | # macOS 81 | return os.path.join(str(Path.home()), "Library", "Application Support", "Code", "User", "globalStorage", "state.vscdb") 82 | else: 83 | # Linux and other Unix-like systems 84 | return os.path.join(str(Path.home()), ".config", "Code", "User", "globalStorage", "state.vscdb") 85 | 86 | 87 | def get_machine_id_path() -> str: 88 | """ 89 | Get the machine ID file path across different platforms. 90 | 91 | Returns: 92 | str: Path to the machine ID file 93 | 94 | Platform specific paths: 95 | - Windows: %APPDATA%/Code/User/machineid 96 | - macOS: ~/Library/Application Support/Code/machineid 97 | - Linux: ~/.config/Code/User/machineid 98 | """ 99 | if sys.platform == "win32": 100 | # Windows 101 | base_path = os.getenv("APPDATA", "") 102 | return os.path.join(base_path, "Code", "User", "machineid") 103 | elif sys.platform == "darwin": 104 | # macOS 105 | return os.path.join(str(Path.home()), "Library", "Application Support", "Code", "machineid") 106 | else: 107 | # Linux and other Unix-like systems 108 | return os.path.join(str(Path.home()), ".config", "Code", "machineid") 109 | 110 | 111 | def get_workspace_storage_path() -> str: 112 | """ 113 | Get the workspaceStorage path across different platforms. 114 | 115 | Returns: 116 | str: Path to the workspaceStorage directory 117 | 118 | Platform specific paths: 119 | - Windows: %APPDATA%/Code/User/workspaceStorage 120 | - macOS: ~/Library/Application Support/Code/User/workspaceStorage 121 | - Linux: ~/.config/Code/User/workspaceStorage 122 | """ 123 | if sys.platform == "win32": 124 | # Windows 125 | base_path = os.getenv("APPDATA", "") 126 | return os.path.join(base_path, "Code", "User", "workspaceStorage") 127 | elif sys.platform == "darwin": 128 | # macOS 129 | return os.path.join(str(Path.home()), "Library", "Application Support", "Code", "User", "workspaceStorage") 130 | else: 131 | # Linux and other Unix-like systems 132 | return os.path.join(str(Path.home()), ".config", "Code", "User", "workspaceStorage") -------------------------------------------------------------------------------- /augutils/workspace_cleaner.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import time 4 | import zipfile 5 | import stat 6 | from utils.paths import get_workspace_storage_path 7 | from pathlib import Path 8 | 9 | def remove_readonly(func, path, excinfo): 10 | """Handle read-only files and directories during deletion""" 11 | try: 12 | os.chmod(path, stat.S_IWRITE) 13 | func(path) 14 | except Exception as e: 15 | return False 16 | return True 17 | 18 | def force_delete_directory(path: Path) -> bool: 19 | """ 20 | Force delete a directory and all its contents. 21 | Returns True if successful, False otherwise. 22 | """ 23 | try: 24 | if os.name == 'nt': 25 | # For Windows, handle read-only files and use long path 26 | path_str = '\\\\?\\' + str(path.resolve()) 27 | shutil.rmtree(path_str, onerror=remove_readonly) 28 | else: 29 | shutil.rmtree(path, onerror=remove_readonly) 30 | return True 31 | except Exception: 32 | return False 33 | 34 | def clean_workspace_storage() -> dict: 35 | """ 36 | Cleans the workspace storage directory after creating a backup. 37 | 38 | This function: 39 | 1. Gets the workspace storage path 40 | 2. Creates a zip backup of all files in the directory 41 | 3. Deletes all files in the directory 42 | 43 | Returns: 44 | dict: A dictionary containing operation results 45 | { 46 | 'backup_path': str, 47 | 'deleted_files_count': int 48 | } 49 | """ 50 | workspace_path = get_workspace_storage_path() 51 | 52 | if not os.path.exists(workspace_path): 53 | raise FileNotFoundError(f"Workspace storage directory not found at: {workspace_path}") 54 | 55 | # Convert to Path object for better path handling 56 | workspace_path = Path(workspace_path) 57 | 58 | # Create backup filename with timestamp 59 | timestamp = int(time.time()) 60 | backup_path = f"{workspace_path}_backup_{timestamp}.zip" 61 | 62 | # Create zip backup 63 | failed_compressions = [] 64 | with zipfile.ZipFile(backup_path, 'w', zipfile.ZIP_DEFLATED) as zipf: 65 | for file_path in workspace_path.rglob('*'): 66 | if file_path.is_file(): 67 | try: 68 | file_path_str = str(file_path) 69 | if os.name == 'nt': 70 | file_path_str = '\\\\?\\' + str(file_path.resolve()) 71 | 72 | arcname = file_path.relative_to(workspace_path) 73 | zipf.write(file_path_str, str(arcname)) 74 | except (OSError, PermissionError, zipfile.BadZipFile) as e: 75 | failed_compressions.append({ 76 | 'file': str(file_path), 77 | 'error': str(e) 78 | }) 79 | continue 80 | 81 | # Count files before deletion 82 | total_files = sum(1 for _ in workspace_path.rglob('*') if _.is_file()) 83 | 84 | # Delete all files in the directory 85 | failed_operations = [] 86 | 87 | def handle_error(e: Exception, path: Path, item_type: str): 88 | failed_operations.append({ 89 | 'type': item_type, 90 | 'path': str(path), 91 | 'error': str(e) 92 | }) 93 | 94 | # First attempt: Try to delete the entire directory tree at once 95 | if not force_delete_directory(workspace_path): 96 | # If bulk deletion fails, try the file-by-file approach 97 | # Delete files first 98 | for file_path in workspace_path.rglob('*'): 99 | if file_path.is_file(): 100 | try: 101 | # Clear read-only attribute if present 102 | if os.name == 'nt': 103 | file_path_str = '\\\\?\\' + str(file_path.resolve()) 104 | os.chmod(file_path_str, stat.S_IWRITE) 105 | else: 106 | os.chmod(str(file_path), stat.S_IWRITE) 107 | 108 | file_path.unlink(missing_ok=True) 109 | except (OSError, PermissionError) as e: 110 | handle_error(e, file_path, 'file') 111 | 112 | # Delete directories from deepest to root 113 | dirs_to_delete = sorted( 114 | [p for p in workspace_path.rglob('*') if p.is_dir()], 115 | key=lambda x: len(str(x).split(os.sep)), 116 | reverse=True 117 | ) 118 | 119 | for dir_path in dirs_to_delete: 120 | try: 121 | # Try force delete first 122 | if not force_delete_directory(dir_path): 123 | # If force delete fails, try regular delete 124 | if os.name == 'nt': 125 | dir_path_str = '\\\\?\\' + str(dir_path.resolve()) 126 | os.rmdir(dir_path_str) 127 | else: 128 | dir_path.rmdir() 129 | except (OSError, PermissionError) as e: 130 | handle_error(e, dir_path, 'directory') 131 | 132 | return { 133 | 'backup_path': str(backup_path), 134 | 'deleted_files_count': total_files, 135 | 'failed_operations': failed_operations, 136 | 'failed_compressions': failed_compressions 137 | } --------------------------------------------------------------------------------