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 +100 -0
- unctools/converter.py +378 -0
- unctools/detector.py +531 -0
- unctools/operations.py +562 -0
- unctools/utils/__init__.py +40 -0
- unctools/utils/compat.py +331 -0
- unctools/utils/logger.py +228 -0
- unctools/utils/validation.py +321 -0
- unctools/windows/__init__.py +45 -0
- unctools/windows/network.py +490 -0
- unctools/windows/registry.py +410 -0
- unctools/windows/security.py +586 -0
- unctools-0.1.0.dist-info/METADATA +189 -0
- unctools-0.1.0.dist-info/RECORD +17 -0
- unctools-0.1.0.dist-info/WHEEL +5 -0
- unctools-0.1.0.dist-info/licenses/LICENSE +21 -0
- unctools-0.1.0.dist-info/top_level.txt +1 -0
unctools/operations.py
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
High-level file operations for working with UNC paths and network drives.
|
|
3
|
+
|
|
4
|
+
This module provides functions for higher-level operations like safely opening files,
|
|
5
|
+
batch converting paths, and copying files with automatic path conversion.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import io
|
|
11
|
+
import logging
|
|
12
|
+
import shutil
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Dict, List, Optional, Union, Callable, TextIO, BinaryIO, Any, Tuple
|
|
15
|
+
|
|
16
|
+
# Import from our own modules
|
|
17
|
+
from .converter import convert_to_local, convert_to_unc, normalize_path
|
|
18
|
+
from .detector import is_unc_path, get_path_type, detect_path_issues, PATH_TYPE_UNC
|
|
19
|
+
|
|
20
|
+
# Set up module-level logger
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
def safe_open(file_path: Union[str, Path], mode: str = 'r',
|
|
24
|
+
encoding: Optional[str] = None, convert_paths: bool = True,
|
|
25
|
+
**kwargs) -> Union[TextIO, BinaryIO]:
|
|
26
|
+
"""
|
|
27
|
+
Safely open a file, handling UNC paths and network drives automatically.
|
|
28
|
+
|
|
29
|
+
This function attempts to open a file, automatically converting UNC paths to
|
|
30
|
+
local drive paths if necessary to avoid permission issues.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
file_path: The path to the file to open.
|
|
34
|
+
mode: The mode to open the file in (same as built-in open).
|
|
35
|
+
encoding: The encoding to use (same as built-in open).
|
|
36
|
+
convert_paths: Whether to automatically convert between UNC and local paths.
|
|
37
|
+
**kwargs: Additional keyword arguments to pass to open.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
A file object, as returned by the built-in open function.
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
FileNotFoundError: If the file does not exist.
|
|
44
|
+
PermissionError: If permission is denied (even after path conversion).
|
|
45
|
+
OSError: If another OS-level error occurs.
|
|
46
|
+
"""
|
|
47
|
+
original_path = Path(file_path)
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
# First try to open the file as-is
|
|
51
|
+
return open(original_path, mode=mode, encoding=encoding, **kwargs)
|
|
52
|
+
except PermissionError:
|
|
53
|
+
# If we get a permission error and conversion is enabled, try converting the path
|
|
54
|
+
if convert_paths:
|
|
55
|
+
try:
|
|
56
|
+
if is_unc_path(original_path):
|
|
57
|
+
# Convert UNC to local
|
|
58
|
+
local_path = convert_to_local(original_path)
|
|
59
|
+
if local_path != original_path:
|
|
60
|
+
logger.debug(f"Converting UNC path {original_path} to local path {local_path}")
|
|
61
|
+
return open(local_path, mode=mode, encoding=encoding, **kwargs)
|
|
62
|
+
else:
|
|
63
|
+
# Convert local to UNC
|
|
64
|
+
unc_path = convert_to_unc(original_path)
|
|
65
|
+
if unc_path != original_path:
|
|
66
|
+
logger.debug(f"Converting local path {original_path} to UNC path {unc_path}")
|
|
67
|
+
return open(unc_path, mode=mode, encoding=encoding, **kwargs)
|
|
68
|
+
except Exception as e:
|
|
69
|
+
logger.warning(f"Path conversion failed: {e}")
|
|
70
|
+
|
|
71
|
+
# If conversion failed or is disabled, re-raise the original error
|
|
72
|
+
raise
|
|
73
|
+
|
|
74
|
+
def file_exists(file_path: Union[str, Path], check_both_paths: bool = True) -> bool:
|
|
75
|
+
"""
|
|
76
|
+
Check if a file exists, handling UNC paths and network drives.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
file_path: The path to check.
|
|
80
|
+
check_both_paths: Whether to check both UNC and local path variants.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
True if the file exists, False otherwise.
|
|
84
|
+
"""
|
|
85
|
+
original_path = Path(file_path)
|
|
86
|
+
|
|
87
|
+
# First check the original path
|
|
88
|
+
if os.path.exists(original_path):
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
# If requested, check the converted path too
|
|
92
|
+
if check_both_paths:
|
|
93
|
+
try:
|
|
94
|
+
if is_unc_path(original_path):
|
|
95
|
+
# Check local path
|
|
96
|
+
local_path = convert_to_local(original_path)
|
|
97
|
+
if local_path != original_path and os.path.exists(local_path):
|
|
98
|
+
return True
|
|
99
|
+
else:
|
|
100
|
+
# Check UNC path
|
|
101
|
+
unc_path = convert_to_unc(original_path)
|
|
102
|
+
if unc_path != original_path and os.path.exists(unc_path):
|
|
103
|
+
return True
|
|
104
|
+
except Exception as e:
|
|
105
|
+
logger.debug(f"Path conversion during file_exists check failed: {e}")
|
|
106
|
+
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
def batch_convert(paths: List[Union[str, Path]], to_unc: bool = False) -> Dict[str, str]:
|
|
110
|
+
"""
|
|
111
|
+
Convert a batch of paths between UNC and local formats.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
paths: List of paths to convert.
|
|
115
|
+
to_unc: If True, convert to UNC paths; if False, convert to local paths.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
A dictionary mapping original paths to converted paths.
|
|
119
|
+
"""
|
|
120
|
+
result = {}
|
|
121
|
+
|
|
122
|
+
for path in paths:
|
|
123
|
+
original_path = str(path)
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
if to_unc:
|
|
127
|
+
converted_path = str(convert_to_unc(path))
|
|
128
|
+
else:
|
|
129
|
+
converted_path = str(convert_to_local(path))
|
|
130
|
+
|
|
131
|
+
result[original_path] = converted_path
|
|
132
|
+
except Exception as e:
|
|
133
|
+
logger.warning(f"Failed to convert path {original_path}: {e}")
|
|
134
|
+
result[original_path] = original_path # Keep original on failure
|
|
135
|
+
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
def safe_copy(src: Union[str, Path], dst: Union[str, Path],
|
|
139
|
+
convert_paths: bool = True, **kwargs) -> str:
|
|
140
|
+
"""
|
|
141
|
+
Safely copy a file, handling UNC paths and network drives.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
src: Source file path.
|
|
145
|
+
dst: Destination file path.
|
|
146
|
+
convert_paths: Whether to automatically convert between UNC and local paths.
|
|
147
|
+
**kwargs: Additional keyword arguments to pass to shutil.copy2.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
The path of the destination file as a string.
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
FileNotFoundError: If the source file does not exist.
|
|
154
|
+
PermissionError: If permission is denied (even after path conversion).
|
|
155
|
+
OSError: If another OS-level error occurs.
|
|
156
|
+
"""
|
|
157
|
+
original_src = Path(src)
|
|
158
|
+
original_dst = Path(dst)
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
# First try to copy the file as-is
|
|
162
|
+
shutil.copy2(original_src, original_dst, **kwargs)
|
|
163
|
+
return str(original_dst) # Always return the destination path string
|
|
164
|
+
except PermissionError:
|
|
165
|
+
# If we get a permission error and conversion is enabled, try converting the paths
|
|
166
|
+
if convert_paths:
|
|
167
|
+
# Try different combinations of path conversions
|
|
168
|
+
path_variants = []
|
|
169
|
+
|
|
170
|
+
# Convert source path
|
|
171
|
+
try:
|
|
172
|
+
if is_unc_path(str(original_src)): # Convert to string before checking
|
|
173
|
+
local_src = convert_to_local(original_src)
|
|
174
|
+
if local_src != original_src:
|
|
175
|
+
path_variants.append((local_src, original_dst))
|
|
176
|
+
else:
|
|
177
|
+
unc_src = convert_to_unc(original_src)
|
|
178
|
+
if unc_src != original_src:
|
|
179
|
+
path_variants.append((unc_src, original_dst))
|
|
180
|
+
except Exception as e:
|
|
181
|
+
logger.debug(f"Source path conversion failed: {e}")
|
|
182
|
+
|
|
183
|
+
# Convert destination path
|
|
184
|
+
try:
|
|
185
|
+
if is_unc_path(str(original_dst)): # Convert to string before checking
|
|
186
|
+
local_dst = convert_to_local(original_dst)
|
|
187
|
+
if local_dst != original_dst:
|
|
188
|
+
path_variants.append((original_src, local_dst))
|
|
189
|
+
else:
|
|
190
|
+
unc_dst = convert_to_unc(original_dst)
|
|
191
|
+
if unc_dst != original_dst:
|
|
192
|
+
path_variants.append((original_src, unc_dst))
|
|
193
|
+
except Exception as e:
|
|
194
|
+
logger.debug(f"Destination path conversion failed: {e}")
|
|
195
|
+
|
|
196
|
+
# Convert both paths
|
|
197
|
+
try:
|
|
198
|
+
if is_unc_path(original_src) and not is_unc_path(original_dst):
|
|
199
|
+
local_src = convert_to_local(original_src)
|
|
200
|
+
unc_dst = convert_to_unc(original_dst)
|
|
201
|
+
if local_src != original_src and unc_dst != original_dst:
|
|
202
|
+
path_variants.append((local_src, unc_dst))
|
|
203
|
+
elif not is_unc_path(original_src) and is_unc_path(original_dst):
|
|
204
|
+
unc_src = convert_to_unc(original_src)
|
|
205
|
+
local_dst = convert_to_local(original_dst)
|
|
206
|
+
if unc_src != original_src and local_dst != original_dst:
|
|
207
|
+
path_variants.append((unc_src, local_dst))
|
|
208
|
+
except Exception as e:
|
|
209
|
+
logger.debug(f"Dual path conversion failed: {e}")
|
|
210
|
+
|
|
211
|
+
# Try each variant
|
|
212
|
+
for src_variant, dst_variant in path_variants:
|
|
213
|
+
try:
|
|
214
|
+
logger.debug(f"Trying copy with converted paths: {src_variant} -> {dst_variant}")
|
|
215
|
+
# Make sure to return a string, not the result from shutil.copy2 which may be a Path
|
|
216
|
+
shutil.copy2(src_variant, dst_variant, **kwargs)
|
|
217
|
+
return str(dst_variant) # Return the destination path as a string
|
|
218
|
+
except Exception as e:
|
|
219
|
+
logger.debug(f"Copy attempt failed: {e}")
|
|
220
|
+
|
|
221
|
+
# If all conversion attempts failed or conversion is disabled, re-raise the original error
|
|
222
|
+
raise
|
|
223
|
+
|
|
224
|
+
def batch_copy(src_paths: List[Union[str, Path]], dst_dir: Union[str, Path],
|
|
225
|
+
convert_paths: bool = True, max_retries: int = 1) -> Dict[str, Tuple[bool, Optional[str]]]:
|
|
226
|
+
"""
|
|
227
|
+
Copy multiple files to a destination directory, handling UNC paths.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
src_paths: List of source file paths to copy.
|
|
231
|
+
dst_dir: Destination directory.
|
|
232
|
+
convert_paths: Whether to automatically convert between UNC and local paths.
|
|
233
|
+
max_retries: Maximum number of retries for each file if copy fails.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
A dictionary mapping source paths to tuples of (success, destination_path).
|
|
237
|
+
If success is False, destination_path will be None.
|
|
238
|
+
"""
|
|
239
|
+
dst_dir_path = Path(dst_dir)
|
|
240
|
+
|
|
241
|
+
# Make sure the destination directory exists
|
|
242
|
+
try:
|
|
243
|
+
os.makedirs(dst_dir_path, exist_ok=True)
|
|
244
|
+
except Exception as e:
|
|
245
|
+
logger.error(f"Failed to create destination directory {dst_dir_path}: {e}")
|
|
246
|
+
|
|
247
|
+
# If conversion is enabled, try with converted path
|
|
248
|
+
if convert_paths:
|
|
249
|
+
try:
|
|
250
|
+
if is_unc_path(str(dst_dir_path)): # Convert Path to string before checking
|
|
251
|
+
local_dst_dir = convert_to_local(dst_dir_path)
|
|
252
|
+
if local_dst_dir != dst_dir_path:
|
|
253
|
+
logger.debug(f"Trying to create directory with converted path: {local_dst_dir}")
|
|
254
|
+
os.makedirs(local_dst_dir, exist_ok=True)
|
|
255
|
+
dst_dir_path = local_dst_dir
|
|
256
|
+
else:
|
|
257
|
+
unc_dst_dir = convert_to_unc(dst_dir_path)
|
|
258
|
+
if unc_dst_dir != dst_dir_path:
|
|
259
|
+
logger.debug(f"Trying to create directory with converted path: {unc_dst_dir}")
|
|
260
|
+
os.makedirs(unc_dst_dir, exist_ok=True)
|
|
261
|
+
dst_dir_path = unc_dst_dir
|
|
262
|
+
except Exception as e2:
|
|
263
|
+
logger.error(f"Failed to create destination directory with converted path: {e2}")
|
|
264
|
+
# Return empty results since we can't proceed
|
|
265
|
+
return {str(src): (False, None) for src in src_paths}
|
|
266
|
+
|
|
267
|
+
# Copy each file
|
|
268
|
+
results = {}
|
|
269
|
+
|
|
270
|
+
for src in src_paths:
|
|
271
|
+
src_path = Path(src)
|
|
272
|
+
filename = src_path.name
|
|
273
|
+
dst_path = dst_dir_path / filename
|
|
274
|
+
|
|
275
|
+
# Try to copy with retries
|
|
276
|
+
success = False
|
|
277
|
+
dst_result = None
|
|
278
|
+
retry_count = 0
|
|
279
|
+
last_error = None
|
|
280
|
+
|
|
281
|
+
while not success and retry_count <= max_retries:
|
|
282
|
+
try:
|
|
283
|
+
# Attempt to copy the file
|
|
284
|
+
dst_result = safe_copy(src_path, dst_path, convert_paths=convert_paths)
|
|
285
|
+
# If we get here, the copy was successful
|
|
286
|
+
success = True
|
|
287
|
+
# Ensure the result is a string for consistency
|
|
288
|
+
dst_result = str(dst_result) if dst_result is not None else None
|
|
289
|
+
except Exception as e:
|
|
290
|
+
last_error = e
|
|
291
|
+
if retry_count < max_retries:
|
|
292
|
+
logger.debug(f"Copy attempt {retry_count + 1} failed for {src_path}, retrying: {e}")
|
|
293
|
+
retry_count += 1
|
|
294
|
+
else:
|
|
295
|
+
# Last attempt failed
|
|
296
|
+
logger.error(f"Failed to copy {src_path} to {dst_path} after {max_retries + 1} attempts: {e}")
|
|
297
|
+
break
|
|
298
|
+
|
|
299
|
+
# Record the result for this file
|
|
300
|
+
if success:
|
|
301
|
+
results[str(src)] = (True, dst_result)
|
|
302
|
+
else:
|
|
303
|
+
logger.error(f"Failed to copy {src_path} to {dst_path}: {last_error}")
|
|
304
|
+
results[str(src)] = (False, None)
|
|
305
|
+
|
|
306
|
+
return results
|
|
307
|
+
|
|
308
|
+
def process_files(directory: Union[str, Path], callback: Callable[[Path], Any],
|
|
309
|
+
pattern: str = "*", recursive: bool = True,
|
|
310
|
+
convert_paths: bool = True) -> Dict[str, Any]:
|
|
311
|
+
"""
|
|
312
|
+
Process files in a directory, handling UNC paths and network drives.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
directory: The directory to process.
|
|
316
|
+
callback: A function to call for each file. It should accept a Path object
|
|
317
|
+
and return any value, which will be included in the results.
|
|
318
|
+
pattern: A glob pattern to match files against.
|
|
319
|
+
recursive: Whether to process subdirectories recursively.
|
|
320
|
+
convert_paths: Whether to automatically convert between UNC and local paths.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
A dictionary mapping file paths to the results of the callback function.
|
|
324
|
+
"""
|
|
325
|
+
dir_path = Path(directory)
|
|
326
|
+
results = {}
|
|
327
|
+
|
|
328
|
+
# Check if we need to try a path conversion
|
|
329
|
+
if not os.path.exists(dir_path) and convert_paths:
|
|
330
|
+
try:
|
|
331
|
+
if is_unc_path(str(dir_path)): # Convert Path to string before checking
|
|
332
|
+
local_dir = convert_to_local(dir_path)
|
|
333
|
+
if local_dir != dir_path and os.path.exists(local_dir):
|
|
334
|
+
logger.debug(f"Converting UNC path {dir_path} to local path {local_dir}")
|
|
335
|
+
dir_path = local_dir
|
|
336
|
+
else:
|
|
337
|
+
unc_dir = convert_to_unc(dir_path)
|
|
338
|
+
if unc_dir != dir_path and os.path.exists(unc_dir):
|
|
339
|
+
logger.debug(f"Converting local path {dir_path} to UNC path {unc_dir}")
|
|
340
|
+
dir_path = unc_dir
|
|
341
|
+
except Exception as e:
|
|
342
|
+
logger.warning(f"Path conversion failed: {e}")
|
|
343
|
+
|
|
344
|
+
# Make sure the directory exists
|
|
345
|
+
if not os.path.exists(dir_path):
|
|
346
|
+
logger.error(f"Directory not found: {dir_path}")
|
|
347
|
+
return results
|
|
348
|
+
|
|
349
|
+
# Process files
|
|
350
|
+
if recursive:
|
|
351
|
+
glob_pattern = f"**/{pattern}"
|
|
352
|
+
else:
|
|
353
|
+
glob_pattern = pattern
|
|
354
|
+
|
|
355
|
+
for file_path in dir_path.glob(glob_pattern):
|
|
356
|
+
if file_path.is_file():
|
|
357
|
+
try:
|
|
358
|
+
# Call the callback function
|
|
359
|
+
result = callback(file_path)
|
|
360
|
+
results[str(file_path)] = result
|
|
361
|
+
except Exception as e:
|
|
362
|
+
logger.error(f"Error processing file {file_path}: {e}")
|
|
363
|
+
results[str(file_path)] = None
|
|
364
|
+
|
|
365
|
+
return results
|
|
366
|
+
|
|
367
|
+
def replace_in_file(file_path: Union[str, Path], old_text: str, new_text: str,
|
|
368
|
+
encoding: str = 'utf-8', convert_paths: bool = True) -> bool:
|
|
369
|
+
"""
|
|
370
|
+
Replace text in a file, handling UNC paths and network drives.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
file_path: The path to the file.
|
|
374
|
+
old_text: The text to replace.
|
|
375
|
+
new_text: The new text.
|
|
376
|
+
encoding: The encoding to use when reading/writing the file.
|
|
377
|
+
convert_paths: Whether to automatically convert between UNC and local paths.
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
True if the file was modified, False otherwise.
|
|
381
|
+
"""
|
|
382
|
+
try:
|
|
383
|
+
# Read the file content
|
|
384
|
+
with safe_open(file_path, 'r', encoding=encoding, convert_paths=convert_paths) as f:
|
|
385
|
+
content = f.read()
|
|
386
|
+
|
|
387
|
+
# Check if the old text exists
|
|
388
|
+
if old_text not in content:
|
|
389
|
+
logger.warning(f"Text not found in file {file_path}")
|
|
390
|
+
return False
|
|
391
|
+
|
|
392
|
+
# Replace the text
|
|
393
|
+
new_content = content.replace(old_text, new_text)
|
|
394
|
+
|
|
395
|
+
# Write back to the file
|
|
396
|
+
with safe_open(file_path, 'w', encoding=encoding, convert_paths=convert_paths) as f:
|
|
397
|
+
f.write(new_content)
|
|
398
|
+
|
|
399
|
+
return True
|
|
400
|
+
except Exception as e:
|
|
401
|
+
logger.error(f"Error replacing text in file {file_path}: {e}")
|
|
402
|
+
return False
|
|
403
|
+
|
|
404
|
+
def batch_replace_in_files(directory: Union[str, Path], old_text: str, new_text: str,
|
|
405
|
+
pattern: str = "*.txt", recursive: bool = True,
|
|
406
|
+
encoding: str = 'utf-8', convert_paths: bool = True) -> Dict[str, bool]:
|
|
407
|
+
"""
|
|
408
|
+
Replace text in multiple files, handling UNC paths and network drives.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
directory: The directory containing files to process.
|
|
412
|
+
old_text: The text to replace.
|
|
413
|
+
new_text: The new text.
|
|
414
|
+
pattern: A glob pattern to match files against.
|
|
415
|
+
recursive: Whether to process subdirectories recursively.
|
|
416
|
+
encoding: The encoding to use when reading/writing the files.
|
|
417
|
+
convert_paths: Whether to automatically convert between UNC and local paths.
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
A dictionary mapping file paths to booleans indicating success.
|
|
421
|
+
"""
|
|
422
|
+
def replace_callback(file_path):
|
|
423
|
+
return replace_in_file(file_path, old_text, new_text, encoding, convert_paths)
|
|
424
|
+
|
|
425
|
+
return process_files(directory, replace_callback, pattern, recursive, convert_paths)
|
|
426
|
+
|
|
427
|
+
def get_unc_path_elements(path: Union[str, Path]) -> Optional[Tuple[str, str, str]]:
|
|
428
|
+
"""
|
|
429
|
+
Extract server, share, and path components from a UNC path.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
path: The UNC path to analyze.
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
A tuple of (server, share, relative_path) if the path is a valid UNC path,
|
|
436
|
+
or None if the path is not a UNC path.
|
|
437
|
+
"""
|
|
438
|
+
original_path_str = str(path)
|
|
439
|
+
path_str = original_path_str.replace('/', '\\')
|
|
440
|
+
|
|
441
|
+
# Check if it's a UNC path
|
|
442
|
+
if not path_str.startswith('\\\\'):
|
|
443
|
+
return None
|
|
444
|
+
|
|
445
|
+
# Parse the UNC path
|
|
446
|
+
match = re.match(r'^\\\\([^\\]+)\\([^\\]+)(?:\\(.*))?', path_str)
|
|
447
|
+
if match:
|
|
448
|
+
server = match.group(1)
|
|
449
|
+
share = match.group(2)
|
|
450
|
+
relative_path = match.group(3) or ""
|
|
451
|
+
|
|
452
|
+
# If original path used forward slashes, preserve them in the relative path
|
|
453
|
+
if '/' in original_path_str:
|
|
454
|
+
# Replace backslashes with forward slashes in the relative path
|
|
455
|
+
relative_path = relative_path.replace('\\', '/')
|
|
456
|
+
|
|
457
|
+
return (server, share, relative_path)
|
|
458
|
+
return None
|
|
459
|
+
|
|
460
|
+
def build_unc_path(server: str, share: str, relative_path: Optional[str] = None) -> str:
|
|
461
|
+
"""
|
|
462
|
+
Build a UNC path from server, share, and path components.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
server: The server name.
|
|
466
|
+
share: The share name.
|
|
467
|
+
relative_path: The relative path within the share (optional).
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
A properly formatted UNC path.
|
|
471
|
+
"""
|
|
472
|
+
unc_base = f"\\\\{server}\\{share}"
|
|
473
|
+
|
|
474
|
+
if not relative_path:
|
|
475
|
+
return unc_base
|
|
476
|
+
|
|
477
|
+
# Ensure relative path doesn't start with a backslash
|
|
478
|
+
rel_path = relative_path.lstrip('\\').lstrip('/')
|
|
479
|
+
|
|
480
|
+
if rel_path:
|
|
481
|
+
return f"{unc_base}\\{rel_path}"
|
|
482
|
+
else:
|
|
483
|
+
return unc_base
|
|
484
|
+
|
|
485
|
+
def is_path_accessible(path: Union[str, Path], check_both_paths: bool = True) -> bool:
|
|
486
|
+
"""
|
|
487
|
+
Check if a path is accessible for reading.
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
path: The path to check.
|
|
491
|
+
check_both_paths: Whether to check both UNC and local path variants.
|
|
492
|
+
|
|
493
|
+
Returns:
|
|
494
|
+
True if the path is accessible, False otherwise.
|
|
495
|
+
"""
|
|
496
|
+
# For files, check if they exist and are readable
|
|
497
|
+
if os.path.isfile(path):
|
|
498
|
+
try:
|
|
499
|
+
with open(path, 'r') as f:
|
|
500
|
+
f.read(1) # Try to read 1 byte
|
|
501
|
+
return True
|
|
502
|
+
except:
|
|
503
|
+
pass
|
|
504
|
+
|
|
505
|
+
# For directories, check if we can list their contents
|
|
506
|
+
elif os.path.isdir(path):
|
|
507
|
+
try:
|
|
508
|
+
os.listdir(path)
|
|
509
|
+
return True
|
|
510
|
+
except:
|
|
511
|
+
pass
|
|
512
|
+
|
|
513
|
+
# If the original path isn't accessible and check_both_paths is enabled
|
|
514
|
+
if check_both_paths:
|
|
515
|
+
try:
|
|
516
|
+
# Try the converted path
|
|
517
|
+
if is_unc_path(str(path)): # Convert Path to string before checking
|
|
518
|
+
local_path = convert_to_local(path)
|
|
519
|
+
if local_path != path:
|
|
520
|
+
return is_path_accessible(local_path, check_both_paths=False)
|
|
521
|
+
else:
|
|
522
|
+
unc_path = convert_to_unc(path)
|
|
523
|
+
if unc_path != path:
|
|
524
|
+
return is_path_accessible(unc_path, check_both_paths=False)
|
|
525
|
+
except Exception as e:
|
|
526
|
+
logger.debug(f"Path conversion during accessibility check failed: {e}")
|
|
527
|
+
|
|
528
|
+
return False
|
|
529
|
+
|
|
530
|
+
def find_accessible_path(path: Union[str, Path]) -> Optional[Path]:
|
|
531
|
+
"""
|
|
532
|
+
Find an accessible variant of a path, trying both UNC and local formats.
|
|
533
|
+
|
|
534
|
+
Args:
|
|
535
|
+
path: The original path to find an accessible variant for.
|
|
536
|
+
|
|
537
|
+
Returns:
|
|
538
|
+
An accessible Path object, or None if no accessible variant is found.
|
|
539
|
+
"""
|
|
540
|
+
original_path = Path(path)
|
|
541
|
+
|
|
542
|
+
# Check the original path first
|
|
543
|
+
if is_path_accessible(original_path, check_both_paths=False):
|
|
544
|
+
return original_path
|
|
545
|
+
|
|
546
|
+
# Try converted paths
|
|
547
|
+
try:
|
|
548
|
+
if is_unc_path(str(original_path)): # Convert Path to string before checking
|
|
549
|
+
# Try local path
|
|
550
|
+
local_path = convert_to_local(original_path)
|
|
551
|
+
if local_path != original_path and is_path_accessible(local_path, check_both_paths=False):
|
|
552
|
+
return local_path
|
|
553
|
+
else:
|
|
554
|
+
# Try UNC path
|
|
555
|
+
unc_path = convert_to_unc(original_path)
|
|
556
|
+
if unc_path != original_path and is_path_accessible(unc_path, check_both_paths=False):
|
|
557
|
+
return unc_path
|
|
558
|
+
except Exception as e:
|
|
559
|
+
logger.debug(f"Path conversion during accessibility check failed: {e}")
|
|
560
|
+
|
|
561
|
+
# If we got here, no accessible variant was found
|
|
562
|
+
return None
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
Utility functions and helpers for UNCtools.
|
|
3
|
+
|
|
4
|
+
This subpackage contains utility functions and helpers used across the UNCtools library,
|
|
5
|
+
including logging, cross-platform compatibility, and validation utilities.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import logging
|
|
10
|
+
import platform
|
|
11
|
+
import sys
|
|
12
|
+
from typing import Dict, List, Optional, Tuple, Union, Any, Callable
|
|
13
|
+
|
|
14
|
+
# Set up module-level logger
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
# Import key functions for the package namespace
|
|
18
|
+
from .logger import configure_logging, get_logger
|
|
19
|
+
from .compat import is_windows, is_linux, is_macos, get_platform_info, is_module_available, safe_import
|
|
20
|
+
from .validation import validate_path, validate_unc_path, validate_local_path
|
|
21
|
+
|
|
22
|
+
# For convenience, re-export platform detection in the utils namespace
|
|
23
|
+
is_windows = is_windows()
|
|
24
|
+
is_linux = is_linux()
|
|
25
|
+
is_macos = is_macos()
|
|
26
|
+
|
|
27
|
+
# Export key functions at the package level
|
|
28
|
+
__all__ = [
|
|
29
|
+
'configure_logging',
|
|
30
|
+
'get_logger',
|
|
31
|
+
'is_windows',
|
|
32
|
+
'is_linux',
|
|
33
|
+
'is_macos',
|
|
34
|
+
'get_platform_info',
|
|
35
|
+
'is_module_available',
|
|
36
|
+
'safe_import',
|
|
37
|
+
'validate_path',
|
|
38
|
+
'validate_unc_path',
|
|
39
|
+
'validate_local_path'
|
|
40
|
+
]
|