unctools 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
unctools/__init__.py ADDED
@@ -0,0 +1,100 @@
1
+ r"""
2
+ UNCtools - A comprehensive toolkit for handling UNC paths, network drives, and substituted drives.
3
+
4
+ This package provides utilities for working with UNC paths (\\server\share) and network/substituted
5
+ drives across different operating systems, with special support for Windows environments.
6
+
7
+ Basic Usage:
8
+ from unctools import convert_to_local, convert_to_unc
9
+
10
+ # Convert UNC path to local drive path
11
+ local_path = convert_to_local("\\\\server\\share\\folder\\file.txt")
12
+
13
+ # Convert local drive path back to UNC
14
+ unc_path = convert_to_unc("Z:\\folder\\file.txt")
15
+
16
+ # Detect path types
17
+ from unctools import is_unc_path, is_network_drive
18
+
19
+ if is_unc_path(path):
20
+ print("This is a UNC path")
21
+
22
+ if is_network_drive("Z:"):
23
+ print("Z: is a network drive")
24
+
25
+ Advanced Windows functionality:
26
+ from unctools.windows import fix_security_zone, get_network_mappings
27
+
28
+ # Fix Windows security zone for a server
29
+ fix_security_zone("server")
30
+
31
+ # Get all network drive mappings
32
+ mappings = get_network_mappings()
33
+ """
34
+
35
+ __version__ = "0.1.0"
36
+
37
+ import os
38
+ import sys
39
+ import logging
40
+ from pathlib import Path
41
+
42
+ # Set up package-level logger
43
+ logger = logging.getLogger(__name__)
44
+
45
+ # Import core functionality into the main namespace
46
+ from .converter import convert_to_local, convert_to_unc, normalize_path
47
+ from .detector import (
48
+ is_unc_path, is_network_drive, is_subst_drive,
49
+ get_path_type, get_network_mappings, detect_path_issues
50
+ )
51
+ from .operations import (
52
+ safe_open, safe_copy, batch_convert, batch_copy,
53
+ process_files, file_exists, replace_in_file, batch_replace_in_files,
54
+ get_unc_path_elements, build_unc_path, is_path_accessible, find_accessible_path
55
+ )
56
+
57
+ # Determine if we're running on Windows
58
+ IS_WINDOWS = os.name == 'nt'
59
+
60
+ # Import Windows-specific modules if on Windows
61
+ if IS_WINDOWS:
62
+ try:
63
+ from .windows import fix_security_zone, add_to_intranet_zone
64
+ except ImportError as e:
65
+ logger.warning(f"Windows-specific modules could not be imported: {e}")
66
+ else:
67
+ # Define stub functions for non-Windows platforms
68
+ def fix_security_zone(server_name):
69
+ """Stub function for non-Windows platforms."""
70
+ logger.warning("fix_security_zone is only available on Windows")
71
+ return False
72
+
73
+ def add_to_intranet_zone(server_name):
74
+ """Stub function for non-Windows platforms."""
75
+ logger.warning("add_to_intranet_zone is only available on Windows")
76
+ return False
77
+
78
+ # Configure default logging
79
+ def configure_logging(level=logging.INFO, handler=None):
80
+ """
81
+ Configure the package's logging settings.
82
+
83
+ Args:
84
+ level: The logging level (default: logging.INFO)
85
+ handler: A logging handler to use (default: StreamHandler)
86
+ """
87
+ if handler is None:
88
+ handler = logging.StreamHandler()
89
+
90
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
91
+ handler.setFormatter(formatter)
92
+
93
+ logger = logging.getLogger(__name__)
94
+ logger.setLevel(level)
95
+ logger.addHandler(handler)
96
+
97
+ # Version information
98
+ def get_version():
99
+ """Return the package version."""
100
+ return __version__
unctools/converter.py ADDED
@@ -0,0 +1,378 @@
1
+ r"""
2
+ Path conversion utilities for UNC paths, network drives, and substituted drives.
3
+
4
+ This module provides functions to convert between UNC paths (\\server\share) and
5
+ local drive paths, as well as normalization and validation functions.
6
+ """
7
+
8
+ import os
9
+ import re
10
+ import logging
11
+ import subprocess
12
+ from pathlib import Path
13
+ from typing import Dict, Union, Optional, Tuple
14
+
15
+ # Set up module-level logger
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Define if we're running on Windows
19
+ IS_WINDOWS = os.name == 'nt'
20
+
21
+ # Global flag for win32net availability
22
+ HAVE_WIN32NET = False
23
+
24
+ # Only try to import Windows-specific modules if we're on Windows
25
+ if IS_WINDOWS:
26
+ from unctools.utils.compat import is_module_available
27
+
28
+ # Check if win32net is available without importing it
29
+ if is_module_available('win32net'):
30
+ try:
31
+ import win32net
32
+ HAVE_WIN32NET = True
33
+ except ImportError:
34
+ # This should rarely happen since we checked for availability
35
+ logger.debug("win32net module found but failed to import.")
36
+
37
+ # Define constants
38
+ UNC_PATTERN = re.compile(r'^\\\\([^\\]+)\\([^\\]+)(?:\\(.*))?$')
39
+ DRIVE_LETTER_PATTERN = re.compile(r'^([A-Za-z]:)(?:\\(.*))?$')
40
+
41
+ class UNCConverter:
42
+ r"""
43
+ Handles conversion between UNC paths and mapped drive paths.
44
+
45
+ This class provides methods to convert UNC paths (\\server\share) to their
46
+ corresponding drive paths (X:\) and vice versa, based on the system's
47
+ network mappings.
48
+ """
49
+
50
+ def __init__(self, refresh_on_init=True):
51
+ """
52
+ Initialize the UNC converter.
53
+
54
+ Args:
55
+ refresh_on_init: Whether to refresh network mappings on initialization.
56
+ Default is True.
57
+ """
58
+ self._mapping: Dict[str, str] = {} # UNC prefix -> drive letter
59
+ self._reverse_mapping: Dict[str, str] = {} # drive letter -> UNC prefix
60
+
61
+ # Windows network share command is only available on Windows
62
+ self._is_windows = IS_WINDOWS
63
+
64
+ if refresh_on_init:
65
+ self.refresh_mappings()
66
+
67
+ def refresh_mappings(self) -> Dict[str, str]:
68
+ """
69
+ Refresh the mapping of UNC paths to drive letters by querying the system.
70
+
71
+ Returns:
72
+ A dictionary mapping UNC prefixes to drive letters.
73
+ """
74
+ if not self._is_windows:
75
+ logger.debug("Not running on Windows, no network mappings to refresh")
76
+ return {}
77
+
78
+ old_mapping = self._mapping.copy()
79
+ self._mapping.clear()
80
+ self._reverse_mapping.clear()
81
+
82
+ # Try to use win32net if available
83
+ if HAVE_WIN32NET:
84
+ success = self._get_mappings_with_win32net()
85
+ if not success:
86
+ # Fall back to subprocess
87
+ self._get_mappings_with_subprocess()
88
+ else:
89
+ # Use subprocess method
90
+ self._get_mappings_with_subprocess()
91
+
92
+ # Check if mappings changed
93
+ if self._mapping != old_mapping:
94
+ logger.debug(f"Network mappings changed: {len(self._mapping)} mappings")
95
+
96
+ return self._mapping
97
+
98
+ def _get_mappings_with_win32net(self) -> bool:
99
+ """
100
+ Get network mappings using the win32net API.
101
+
102
+ This method populates the internal mapping dictionaries.
103
+
104
+ Returns:
105
+ True if successful, False otherwise.
106
+ """
107
+ if not HAVE_WIN32NET:
108
+ return False
109
+
110
+ try:
111
+ # Import here to ensure it's only imported when needed
112
+ import win32net
113
+
114
+ # Level 2 provides detailed information
115
+ connections, total, resume = win32net.NetUseEnum(None, 2)
116
+
117
+ for conn in connections:
118
+ local = conn.get('local', '').upper()
119
+ remote = conn.get('remote', '').lower()
120
+
121
+ if local and remote:
122
+ # Ensure drive letter has trailing backslash
123
+ if local and not local.endswith('\\'):
124
+ local += '\\'
125
+
126
+ # Store UNC path without trailing backslash as key
127
+ remote = remote.rstrip('\\')
128
+
129
+ self._mapping[remote] = local
130
+ self._reverse_mapping[local.rstrip('\\')] = remote
131
+
132
+ logger.debug(f"Retrieved {len(self._mapping)} network mappings using win32net")
133
+ return True
134
+ except Exception as e:
135
+ logger.warning(f"Error in win32net.NetUseEnum: {e}")
136
+ return False
137
+
138
+ def _get_mappings_with_subprocess(self) -> None:
139
+ """
140
+ Get network mappings by parsing 'net use' command output.
141
+
142
+ This method populates the internal mapping dictionaries.
143
+ """
144
+ if not self._is_windows:
145
+ return
146
+
147
+ try:
148
+ output = subprocess.check_output(["net", "use"], text=True, stderr=subprocess.STDOUT)
149
+
150
+ # Parse output and extract mappings
151
+ for line in output.splitlines():
152
+ # Look for lines like: "OK Z: \\server\share"
153
+ m = re.search(r'^(OK|Disconnected)\s+([A-Za-z]:)\s+(\\\\\S+)', line, re.IGNORECASE)
154
+ if m:
155
+ drive_letter = m.group(2).upper()
156
+ if not drive_letter.endswith('\\'):
157
+ drive_letter += '\\'
158
+
159
+ unc_path = m.group(3).lower().rstrip('\\')
160
+
161
+ self._mapping[unc_path] = drive_letter
162
+ self._reverse_mapping[drive_letter.rstrip('\\')] = unc_path
163
+
164
+ logger.debug(f"Retrieved {len(self._mapping)} network mappings using 'net use'")
165
+ except Exception as e:
166
+ logger.warning(f"Failed to get network mappings using 'net use': {e}")
167
+
168
+ def convert_to_local(self, path: Union[str, Path]) -> Path:
169
+ """
170
+ Convert a UNC path to its corresponding local drive path if possible.
171
+
172
+ Args:
173
+ path: The path to convert, potentially a UNC path.
174
+
175
+ Returns:
176
+ Path: The converted path using a drive letter if a mapping exists,
177
+ otherwise the original path.
178
+ """
179
+ path_str = str(path).replace('/', '\\')
180
+
181
+ # If the path already has a drive letter, return it unchanged
182
+ if re.match(r'^[A-Za-z]:', path_str):
183
+ return Path(path_str)
184
+
185
+ # Check if it's a UNC path (starts with \\)
186
+ if not path_str.startswith('\\\\'):
187
+ return Path(path_str)
188
+
189
+ # Try to match the UNC path with known mappings
190
+ # Sort keys by length in descending order to match the most specific first
191
+ for unc_prefix in sorted(self._mapping.keys(), key=len, reverse=True):
192
+ if path_str.lower().startswith(unc_prefix.lower()):
193
+ # Replace the UNC prefix with the drive letter
194
+ local_part = path_str[len(unc_prefix):]
195
+ drive_path = f"{self._mapping[unc_prefix]}{local_part.lstrip(chr(92))}"
196
+ logger.debug(f"Converted UNC path '{path_str}' to local path '{drive_path}'")
197
+ return Path(drive_path)
198
+
199
+ # No matching mapping found, return the original path
200
+ logger.debug(f"No drive mapping found for UNC path '{path_str}'")
201
+ return Path(path_str)
202
+
203
+ def convert_to_unc(self, path: Union[str, Path]) -> Path:
204
+ """
205
+ Convert a local drive path to its corresponding UNC path if possible.
206
+
207
+ Args:
208
+ path: The path to convert, potentially using a mapped drive.
209
+
210
+ Returns:
211
+ Path: The converted UNC path if the drive is mapped to a network share,
212
+ otherwise the original path.
213
+ """
214
+ path_str = str(path).replace('/', '\\')
215
+
216
+ # Check if the path starts with a drive letter
217
+ match = re.match(r'^([A-Za-z]:[\\]?)', path_str, re.IGNORECASE)
218
+ if not match:
219
+ # Not a drive path, return unchanged
220
+ return Path(path_str)
221
+
222
+ drive = match.group(1).upper()
223
+ if not drive.endswith('\\'):
224
+ drive += '\\'
225
+
226
+ drive_no_slash = drive.rstrip('\\')
227
+
228
+ # Check if the drive is in our mapping
229
+ if drive_no_slash in self._reverse_mapping:
230
+ unc_prefix = self._reverse_mapping[drive_no_slash]
231
+ # Replace the drive with the UNC path
232
+ rest_of_path = path_str[len(drive_no_slash):].lstrip(chr(92))
233
+ unc_path = f"{unc_prefix}{chr(92)}{rest_of_path}"
234
+ logger.debug(f"Converted local path '{path_str}' to UNC path '{unc_path}'")
235
+ return Path(unc_path)
236
+
237
+ # No matching mapping found, return the original path
238
+ logger.debug(f"No UNC mapping found for local path '{path_str}'")
239
+ return Path(path_str)
240
+
241
+ def get_mappings(self) -> Dict[str, str]:
242
+ """
243
+ Get a dictionary of current UNC path to drive letter mappings.
244
+
245
+ Returns:
246
+ A dictionary mapping UNC paths to drive letters.
247
+ """
248
+ return self._mapping.copy()
249
+
250
+ def get_reverse_mappings(self) -> Dict[str, str]:
251
+ """
252
+ Get a dictionary of current drive letter to UNC path mappings.
253
+
254
+ Returns:
255
+ A dictionary mapping drive letters to UNC paths.
256
+ """
257
+ return self._reverse_mapping.copy()
258
+
259
+ # Create a global instance for convenience
260
+ _global_converter = None
261
+
262
+ def _get_global_converter() -> UNCConverter:
263
+ """
264
+ Get or create the global UNCConverter instance.
265
+
266
+ Returns:
267
+ The global UNCConverter instance.
268
+ """
269
+ global _global_converter
270
+ if _global_converter is None:
271
+ _global_converter = UNCConverter()
272
+ return _global_converter
273
+
274
+ def convert_to_local(path: Union[str, Path]) -> Path:
275
+ """
276
+ Convert a UNC path to its corresponding local drive path if possible.
277
+
278
+ Args:
279
+ path: The path to convert, potentially a UNC path.
280
+
281
+ Returns:
282
+ Path: The converted path using a drive letter if a mapping exists,
283
+ otherwise the original path.
284
+ """
285
+ converter = _get_global_converter()
286
+ return converter.convert_to_local(path)
287
+
288
+ def convert_to_unc(path: Union[str, Path]) -> Path:
289
+ """
290
+ Convert a local drive path to its corresponding UNC path if possible.
291
+
292
+ Args:
293
+ path: The path to convert, potentially using a mapped drive.
294
+
295
+ Returns:
296
+ Path: The converted UNC path if the drive is mapped to a network share,
297
+ otherwise the original path.
298
+ """
299
+ converter = _get_global_converter()
300
+ return converter.convert_to_unc(path)
301
+
302
+ def refresh_mappings() -> Dict[str, str]:
303
+ """
304
+ Refresh the global mapping of UNC paths to drive letters.
305
+
306
+ Returns:
307
+ A dictionary mapping UNC paths to drive letters.
308
+ """
309
+ converter = _get_global_converter()
310
+ return converter.refresh_mappings()
311
+
312
+ def get_mappings() -> Dict[str, str]:
313
+ """
314
+ Get the current global UNC path to drive letter mappings.
315
+
316
+ Returns:
317
+ A dictionary mapping UNC paths to drive letters.
318
+ """
319
+ converter = _get_global_converter()
320
+ return converter.get_mappings()
321
+
322
+ def normalize_path(path: Union[str, Path], prefer_unc: bool = False) -> Path:
323
+ """
324
+ Normalize a path by ensuring consistent format and optionally converting between UNC and local.
325
+
326
+ Args:
327
+ path: The path to normalize.
328
+ prefer_unc: If True, convert local paths to UNC if possible.
329
+ If False, convert UNC paths to local if possible.
330
+
331
+ Returns:
332
+ The normalized path.
333
+ """
334
+ path_obj = Path(str(path).replace('/', '\\'))
335
+
336
+ if prefer_unc:
337
+ return convert_to_unc(path_obj)
338
+ else:
339
+ return convert_to_local(path_obj)
340
+
341
+ def parse_unc_path(path: Union[str, Path]) -> Optional[Tuple[str, str, str]]:
342
+ """
343
+ Parse a UNC path into server, share, and path components.
344
+
345
+ Args:
346
+ path: The UNC path to parse.
347
+
348
+ Returns:
349
+ A tuple of (server, share, path) if the path is a valid UNC path,
350
+ or None if the path is not a UNC path.
351
+ """
352
+ path_str = str(path).replace('/', '\\')
353
+ match = UNC_PATTERN.match(path_str)
354
+
355
+ if match:
356
+ server = match.group(1)
357
+ share = match.group(2)
358
+ rest = match.group(3) or ""
359
+ return (server, share, rest)
360
+
361
+ return None
362
+
363
+ def join_unc_path(server: str, share: str, rest: str = "") -> str:
364
+ """
365
+ Join UNC path components into a complete UNC path.
366
+
367
+ Args:
368
+ server: The server name.
369
+ share: The share name.
370
+ rest: The rest of the path (optional).
371
+
372
+ Returns:
373
+ A properly formatted UNC path.
374
+ """
375
+ if rest:
376
+ return f"{chr(92)}{chr(92)}{server}{chr(92)}{share}{chr(92)}{rest.lstrip(chr(92))}"
377
+ else:
378
+ return f"{chr(92)}{chr(92)}{server}{chr(92)}{share}"