Coverage for src / ezcompiler / adapters / disk_uploader.py: 22.67%
55 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-27 06:49 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-27 06:49 +0000
1# ///////////////////////////////////////////////////////////////
2# DISK_UPLOADER - Local disk uploader implementation
3# Project: ezcompiler
4# ///////////////////////////////////////////////////////////////
6"""
7Disk uploader - Local disk upload handler for EzCompiler.
9This module provides functionality for uploading files and directories to
10local disk locations, with support for backup creation, permission preservation,
11and overwrite control.
13Note: Protocols layer should not perform logging directly. Logging is handled
14by the service layer that orchestrates upload operations.
15"""
17from __future__ import annotations
19# ///////////////////////////////////////////////////////////////
20# IMPORTS
21# ///////////////////////////////////////////////////////////////
22# Standard library imports
23import contextlib
24import shutil
25from pathlib import Path
26from typing import Any
28# Local imports
29from ..shared.exceptions import UploadError
30from ..utils.file_utils import FileUtils
31from ..utils.uploader_utils import UploaderUtils
32from .base_uploader import BaseUploader
34# ///////////////////////////////////////////////////////////////
35# CLASSES
36# ///////////////////////////////////////////////////////////////
39class DiskUploader(BaseUploader):
40 """
41 Uploader for local disk operations.
43 Handles copying files and directories to local disk locations with
44 configurable behavior for permissions, overwrites, and backups.
46 Configuration keys:
47 preserve_permissions (bool): Preserve file permissions (default: True)
48 overwrite (bool): Allow overwriting existing files (default: True)
49 create_backup (bool): Create backup before overwrite (default: False)
51 Example:
52 >>> config = {"preserve_permissions": True, "overwrite": True}
53 >>> uploader = DiskUploader(config)
54 >>> uploader.upload(Path("source.zip"), "/path/to/destination.zip")
55 """
57 # ////////////////////////////////////////////////
58 # INITIALIZATION
59 # ////////////////////////////////////////////////
61 def __init__(self, config: dict[str, Any] | None = None) -> None:
62 """
63 Initialize the disk uploader.
65 Args:
66 config: Optional configuration dictionary with keys:
67 - preserve_permissions (bool): Preserve file permissions
68 - overwrite (bool): Allow overwriting existing files
69 - create_backup (bool): Create backup before overwrite
70 """
71 default_config = UploaderUtils.get_default_disk_config()
73 if config:
74 default_config.update(config)
76 super().__init__(default_config)
78 # ////////////////////////////////////////////////
79 # PUBLIC METHODS
80 # ////////////////////////////////////////////////
82 def get_uploader_name(self) -> str:
83 """
84 Get the name of this uploader.
86 Returns:
87 str: Name of the uploader
88 """
89 return "Disk Uploader"
91 def upload(self, source_path: Path, destination: str) -> None:
92 """
93 Upload a file or directory to a local disk location.
95 Args:
96 source_path: Path to the source file or directory
97 destination: Destination path on local disk
99 Raises:
100 UploadError: If upload fails
102 Note:
103 Creates parent directories automatically if they don't exist.
104 """
105 try:
106 self._validate_source_path(source_path)
107 dest_path = Path(destination)
109 # For file uploads, treat destination as a directory and
110 # preserve the source filename (e.g., "releases/Nuitka" + "build.zip"
111 # -> "releases/Nuitka/build.zip")
112 if source_path.is_file():
113 FileUtils.create_directory_if_not_exists(dest_path)
114 dest_path = dest_path / source_path.name
115 else:
116 FileUtils.create_directory_if_not_exists(dest_path)
118 # Handle existing destination
119 if not self._config["overwrite"] and dest_path.exists():
120 if self._config["create_backup"]:
121 self._create_backup(dest_path)
122 else:
123 raise UploadError(
124 f"Destination already exists and overwrite is disabled: {dest_path}"
125 )
127 # Perform upload
128 if source_path.is_file():
129 self._upload_file(source_path, dest_path)
130 else:
131 self._upload_directory(source_path, dest_path)
133 except UploadError:
134 raise
135 except Exception as e:
136 raise UploadError(f"Disk upload failed: {e}") from e
138 # ////////////////////////////////////////////////
139 # PRIVATE METHODS
140 # ////////////////////////////////////////////////
142 def _upload_file(self, source_path: Path, dest_path: Path) -> None:
143 """
144 Upload a single file.
146 Args:
147 source_path: Source file path
148 dest_path: Destination file path
150 Note:
151 Uses copy2 to preserve metadata, then optionally preserves permissions.
152 """
153 shutil.copy2(source_path, dest_path)
155 # Preserve permissions if configured
156 if self._config["preserve_permissions"]:
157 with contextlib.suppress(OSError, AttributeError):
158 shutil.copystat(source_path, dest_path)
160 def _upload_directory(self, source_path: Path, dest_path: Path) -> None:
161 """
162 Upload a directory recursively.
164 Args:
165 source_path: Source directory path
166 dest_path: Destination directory path
168 Note:
169 Removes destination if it exists and overwrite is enabled.
170 """
171 if dest_path.exists() and self._config["overwrite"]:
172 shutil.rmtree(dest_path)
174 shutil.copytree(
175 source_path,
176 dest_path,
177 dirs_exist_ok=self._config["overwrite"],
178 copy_function=(
179 shutil.copy2 if self._config["preserve_permissions"] else shutil.copy
180 ),
181 )
183 def _create_backup(self, file_path: Path) -> None:
184 """
185 Create a backup of an existing file.
187 Args:
188 file_path: Path to file to backup
190 Note:
191 Uses UploaderUtils to generate unique backup path.
192 """
193 backup_path = UploaderUtils.generate_backup_path(file_path)
194 shutil.copy2(file_path, backup_path)
196 # ////////////////////////////////////////////////
197 # VALIDATION METHODS
198 # ////////////////////////////////////////////////
200 def _validate_config(self) -> None:
201 """
202 Validate disk uploader configuration.
204 Raises:
205 UploadError: If configuration is invalid
207 Note:
208 Ensures all required boolean flags are present and valid.
209 """
210 required_keys = ["preserve_permissions", "overwrite", "create_backup"]
212 for key in required_keys:
213 if key not in self._config:
214 raise UploadError(f"Missing required configuration key: {key}")
216 if not isinstance(self._config[key], bool):
217 raise UploadError(f"Configuration key '{key}' must be a boolean")