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.
@@ -0,0 +1,490 @@
1
+ """
2
+ Windows network utilities for managing network drives and connections.
3
+
4
+ This module provides functions for working with Windows network drives,
5
+ including creating and removing network mappings, checking network connectivity,
6
+ and managing network shares.
7
+ """
8
+
9
+ import os
10
+ import re
11
+ import logging
12
+ import subprocess
13
+ from pathlib import Path
14
+ from typing import Dict, List, Optional, Tuple, Union, Any
15
+
16
+ # Set up module-level logger
17
+ logger = logging.getLogger(__name__)
18
+
19
+ # Check if we're running on Windows
20
+ IS_WINDOWS = os.name == 'nt'
21
+
22
+ # Try to import Windows-specific modules
23
+ if IS_WINDOWS:
24
+ try:
25
+ import win32net
26
+ import win32wnet
27
+ import win32api
28
+ import win32con
29
+ import win32netcon
30
+ from win32file import WNetAddConnection2, WNetCancelConnection2
31
+ HAVE_WIN32NET = True
32
+ except ImportError:
33
+ HAVE_WIN32NET = False
34
+ logger.debug("win32net module not available. Using command-line fallbacks for network operations.")
35
+ else:
36
+ HAVE_WIN32NET = False
37
+
38
+ def create_network_mapping(unc_path: str, drive_letter: Optional[str] = None,
39
+ username: Optional[str] = None, password: Optional[str] = None,
40
+ persistent: bool = False) -> Tuple[bool, Optional[str]]:
41
+ """
42
+ Create a network drive mapping for a UNC path.
43
+
44
+ Args:
45
+ unc_path: The UNC path to map (e.g., \\\\server\\share).
46
+ drive_letter: The drive letter to map to (e.g., "Z:").
47
+ If None, Windows will assign the next available drive.
48
+ username: The username for authentication (optional).
49
+ password: The password for authentication (optional).
50
+ persistent: Whether the mapping should persist across reboots.
51
+
52
+ Returns:
53
+ A tuple of (success, drive_letter), where success is a boolean indicating
54
+ whether the operation was successful, and drive_letter is the assigned
55
+ drive letter (which may be different from the requested one if automatic
56
+ assignment was used).
57
+ """
58
+ if not IS_WINDOWS:
59
+ logger.warning("Network mappings are only available on Windows.")
60
+ return (False, None)
61
+
62
+ # Normalize UNC path
63
+ unc_path = unc_path.replace('/', '\\')
64
+ if not unc_path.startswith('\\\\'):
65
+ unc_path = '\\\\' + unc_path.lstrip('\\')
66
+
67
+ # Normalize drive letter if provided
68
+ if drive_letter:
69
+ drive_letter = drive_letter.upper()
70
+ if not drive_letter.endswith(':'):
71
+ drive_letter += ':'
72
+
73
+ # Try to use the Windows API if available
74
+ if HAVE_WIN32NET:
75
+ try:
76
+ # Create a network resource object
77
+ netresource = {
78
+ 'Provider': None,
79
+ 'Flags': 0,
80
+ 'LocalName': drive_letter if drive_letter else None,
81
+ 'RemoteName': unc_path
82
+ }
83
+
84
+ # Attempt to add the connection
85
+ try:
86
+ WNetAddConnection2(netresource, password, username, win32con.CONNECT_UPDATE_PROFILE if persistent else 0)
87
+
88
+ # If we didn't specify a drive letter, we need to find out what was assigned
89
+ if not drive_letter:
90
+ # Enumerate network drives to find our newly created one
91
+ connections, _, _ = win32net.NetUseEnum(None, 2)
92
+ for conn in connections:
93
+ if conn.get('remote', '').lower() == unc_path.lower():
94
+ drive_letter = conn.get('local', '').upper()
95
+ break
96
+
97
+ logger.info(f"Successfully mapped {unc_path} to {drive_letter}")
98
+ return (True, drive_letter)
99
+ except win32api.error as e:
100
+ logger.error(f"Failed to map network drive using WNetAddConnection2: {e}")
101
+ # Fall back to net use command
102
+ pass
103
+ except Exception as e:
104
+ logger.error(f"Error using Windows API for network mapping: {e}")
105
+ # Fall back to net use command
106
+
107
+ # Fall back to using the net use command
108
+ cmd = ['net', 'use']
109
+
110
+ # Add drive letter if specified
111
+ if drive_letter:
112
+ cmd.append(drive_letter)
113
+
114
+ # Add UNC path
115
+ cmd.append(unc_path)
116
+
117
+ # Add password if specified
118
+ if password:
119
+ cmd.append(password)
120
+
121
+ # Add persistence flag
122
+ if persistent:
123
+ cmd.append('/PERSISTENT:YES')
124
+ else:
125
+ cmd.append('/PERSISTENT:NO')
126
+
127
+ # Add username if specified
128
+ if username:
129
+ cmd.append(f'/USER:{username}')
130
+
131
+ try:
132
+ # Execute the command
133
+ result = subprocess.run(cmd, text=True, capture_output=True, check=False)
134
+
135
+ if result.returncode == 0:
136
+ # Command succeeded, parse output to find drive letter if not specified
137
+ if not drive_letter:
138
+ # Try to extract from output - "Drive Z: is now connected to \\server\share"
139
+ match = re.search(r'Drive ([A-Z]:)', result.stdout)
140
+ if match:
141
+ drive_letter = match.group(1)
142
+
143
+ logger.info(f"Successfully mapped {unc_path} to {drive_letter}")
144
+ return (True, drive_letter)
145
+ else:
146
+ logger.error(f"Failed to map network drive: {result.stderr}")
147
+ return (False, None)
148
+ except Exception as e:
149
+ logger.error(f"Error executing net use command: {e}")
150
+ return (False, None)
151
+
152
+ def remove_network_mapping(drive_letter: str, force: bool = False) -> bool:
153
+ """
154
+ Remove a network drive mapping.
155
+
156
+ Args:
157
+ drive_letter: The drive letter to remove (e.g., "Z:").
158
+ force: Whether to force disconnection even if the drive is in use.
159
+
160
+ Returns:
161
+ True if the operation was successful, False otherwise.
162
+ """
163
+ if not IS_WINDOWS:
164
+ logger.warning("Network mappings are only available on Windows.")
165
+ return False
166
+
167
+ # Normalize drive letter
168
+ drive_letter = drive_letter.upper()
169
+ if not drive_letter.endswith(':'):
170
+ drive_letter += ':'
171
+
172
+ # Try to use the Windows API if available
173
+ if HAVE_WIN32NET:
174
+ try:
175
+ # Attempt to cancel the connection
176
+ WNetCancelConnection2(drive_letter, win32con.CONNECT_UPDATE_PROFILE, force)
177
+ logger.info(f"Successfully removed network mapping for {drive_letter}")
178
+ return True
179
+ except win32api.error as e:
180
+ logger.warning(f"Failed to remove network mapping using WNetCancelConnection2: {e}")
181
+ # Fall back to net use command
182
+
183
+ # Fall back to using the net use command
184
+ cmd = ['net', 'use', drive_letter, '/DELETE']
185
+
186
+ if force:
187
+ cmd.append('/Y')
188
+
189
+ try:
190
+ # Execute the command
191
+ result = subprocess.run(cmd, text=True, capture_output=True, check=False)
192
+
193
+ if result.returncode == 0:
194
+ logger.info(f"Successfully removed network mapping for {drive_letter}")
195
+ return True
196
+ else:
197
+ logger.error(f"Failed to remove network mapping: {result.stderr}")
198
+ return False
199
+ except Exception as e:
200
+ logger.error(f"Error executing net use command: {e}")
201
+ return False
202
+
203
+ def get_all_network_mappings() -> Dict[str, str]:
204
+ """
205
+ Get all network drive mappings.
206
+
207
+ Returns:
208
+ A dictionary mapping drive letters to UNC paths.
209
+ """
210
+ if not IS_WINDOWS:
211
+ logger.warning("Network mappings are only available on Windows.")
212
+ return {}
213
+
214
+ mappings = {}
215
+
216
+ # Try to use the Windows API if available
217
+ if HAVE_WIN32NET:
218
+ try:
219
+ # Enumerate network connections
220
+ connections, _, _ = win32net.NetUseEnum(None, 2)
221
+
222
+ for conn in connections:
223
+ local = conn.get('local', '')
224
+ remote = conn.get('remote', '')
225
+
226
+ if local and remote:
227
+ mappings[local.upper()] = remote
228
+
229
+ return mappings
230
+ except Exception as e:
231
+ logger.warning(f"Failed to enumerate network mappings using NetUseEnum: {e}")
232
+ # Fall back to net use command
233
+
234
+ # Fall back to using the net use command
235
+ try:
236
+ result = subprocess.run(['net', 'use'], text=True, capture_output=True, check=False)
237
+
238
+ if result.returncode == 0:
239
+ # Parse the output to extract mappings
240
+ for line in result.stdout.splitlines():
241
+ # Look for lines like "OK Z: \\server\share"
242
+ match = re.search(r'(OK|Disconnected)\s+([A-Za-z]:)\s+(\\\\\S+)', line, re.IGNORECASE)
243
+ if match:
244
+ drive_letter = match.group(2).upper()
245
+ unc_path = match.group(3)
246
+ mappings[drive_letter] = unc_path
247
+
248
+ return mappings
249
+ else:
250
+ logger.error(f"Failed to enumerate network mappings: {result.stderr}")
251
+ return {}
252
+ except Exception as e:
253
+ logger.error(f"Error executing net use command: {e}")
254
+ return {}
255
+
256
+ def check_network_connection(server: str, timeout: int = 5) -> bool:
257
+ """
258
+ Check if a network server is reachable.
259
+
260
+ Args:
261
+ server: The server name or IP address to check.
262
+ timeout: The timeout in seconds.
263
+
264
+ Returns:
265
+ True if the server is reachable, False otherwise.
266
+ """
267
+ if not IS_WINDOWS:
268
+ # On non-Windows platforms, try ping
269
+ try:
270
+ result = subprocess.run(['ping', '-c', '1', '-W', str(timeout), server],
271
+ text=True, capture_output=True, check=False)
272
+ return result.returncode == 0
273
+ except Exception as e:
274
+ logger.error(f"Error executing ping command: {e}")
275
+ return False
276
+
277
+ # On Windows, try ping with Windows-specific arguments
278
+ try:
279
+ result = subprocess.run(['ping', '-n', '1', '-w', str(timeout * 1000), server],
280
+ text=True, capture_output=True, check=False)
281
+ return result.returncode == 0
282
+ except Exception as e:
283
+ logger.error(f"Error executing ping command: {e}")
284
+ return False
285
+
286
+ def get_server_shares(server: str, username: Optional[str] = None,
287
+ password: Optional[str] = None) -> List[str]:
288
+ """
289
+ Get a list of shares available on a server.
290
+
291
+ Args:
292
+ server: The server name.
293
+ username: The username for authentication (optional).
294
+ password: The password for authentication (optional).
295
+
296
+ Returns:
297
+ A list of share names.
298
+ """
299
+ if not IS_WINDOWS:
300
+ logger.warning("Network share enumeration is only available on Windows.")
301
+ return []
302
+
303
+ shares = []
304
+
305
+ # Try to use the Windows API if available
306
+ if HAVE_WIN32NET:
307
+ try:
308
+ # Build server UNC path
309
+ server_unc = '\\\\' + server.strip('\\')
310
+
311
+ # Set up authentication if provided
312
+ if username and password:
313
+ try:
314
+ # Use NetUseAdd to create a temporary connection
315
+ use_info = {
316
+ 'remote': server_unc,
317
+ 'password': password,
318
+ 'username': username
319
+ }
320
+ win32net.NetUseAdd(None, 2, use_info)
321
+
322
+ # Clean up when we're done
323
+ temp_connection_added = True
324
+ except Exception as e:
325
+ logger.warning(f"Failed to create temporary connection: {e}")
326
+ temp_connection_added = False
327
+ else:
328
+ temp_connection_added = False
329
+
330
+ try:
331
+ # Enumerate shares
332
+ share_info, _, _ = win32net.NetShareEnum(server, 1)
333
+
334
+ for share in share_info:
335
+ share_name = share.get('netname', '')
336
+ if share_name and not share_name.endswith('$'): # Exclude hidden shares
337
+ shares.append(share_name)
338
+ finally:
339
+ # Clean up temporary connection if we created one
340
+ if temp_connection_added:
341
+ try:
342
+ win32net.NetUseDel(None, server_unc)
343
+ except:
344
+ pass
345
+
346
+ return shares
347
+ except Exception as e:
348
+ logger.warning(f"Failed to enumerate shares using NetShareEnum: {e}")
349
+ # Fall back to net view command
350
+
351
+ # Fall back to using the net view command
352
+ cmd = ['net', 'view', '\\\\' + server.strip('\\')]
353
+
354
+ try:
355
+ result = subprocess.run(cmd, text=True, capture_output=True, check=False)
356
+
357
+ if result.returncode == 0:
358
+ # Parse the output to extract share names
359
+ for line in result.stdout.splitlines():
360
+ # Look for lines with share names
361
+ match = re.search(r'(\S+)\s+Disk\s+', line, re.IGNORECASE)
362
+ if match:
363
+ share_name = match.group(1)
364
+ if not share_name.endswith('$'): # Exclude hidden shares
365
+ shares.append(share_name)
366
+
367
+ return shares
368
+ else:
369
+ logger.error(f"Failed to enumerate shares: {result.stderr}")
370
+ return []
371
+ except Exception as e:
372
+ logger.error(f"Error executing net view command: {e}")
373
+ return []
374
+
375
+ def create_share(path: str, share_name: str, description: str = "",
376
+ max_users: int = -1, full_access_users: List[str] = None) -> bool:
377
+ """
378
+ Create a network share on the local machine.
379
+
380
+ Args:
381
+ path: The local path to share.
382
+ share_name: The name of the share.
383
+ description: A description of the share.
384
+ max_users: Maximum number of concurrent users (-1 for unlimited).
385
+ full_access_users: List of users to grant full access to.
386
+
387
+ Returns:
388
+ True if the operation was successful, False otherwise.
389
+ """
390
+ if not IS_WINDOWS:
391
+ logger.warning("Network share creation is only available on Windows.")
392
+ return False
393
+
394
+ # Check if path exists
395
+ if not os.path.exists(path):
396
+ logger.error(f"Path does not exist: {path}")
397
+ return False
398
+
399
+ # Try to use the Windows API if available
400
+ if HAVE_WIN32NET:
401
+ try:
402
+ # Create the share
403
+ share_info = {
404
+ 'netname': share_name,
405
+ 'path': os.path.abspath(path),
406
+ 'remark': description,
407
+ 'max_uses': max_users,
408
+ 'type': win32netcon.STYPE_DISKTREE,
409
+ 'permissions': 0
410
+ }
411
+
412
+ win32net.NetShareAdd(None, 2, share_info)
413
+
414
+ # Set permissions if requested
415
+ if full_access_users:
416
+ for user in full_access_users:
417
+ # Add permission setting code here
418
+ # This requires more complex Windows API calls
419
+ pass
420
+
421
+ logger.info(f"Successfully created share {share_name} for {path}")
422
+ return True
423
+ except Exception as e:
424
+ logger.error(f"Failed to create share using NetShareAdd: {e}")
425
+ # Fall back to net share command
426
+
427
+ # Fall back to using the net share command
428
+ cmd = ['net', 'share', share_name + '=' + os.path.abspath(path)]
429
+
430
+ if description:
431
+ cmd.extend(['/REMARK:', description])
432
+
433
+ if max_users >= 0:
434
+ cmd.extend(['/USERS:', str(max_users)])
435
+
436
+ try:
437
+ result = subprocess.run(cmd, text=True, capture_output=True, check=False)
438
+
439
+ if result.returncode == 0:
440
+ logger.info(f"Successfully created share {share_name} for {path}")
441
+
442
+ # Additional commands would be needed to set permissions
443
+
444
+ return True
445
+ else:
446
+ logger.error(f"Failed to create share: {result.stderr}")
447
+ return False
448
+ except Exception as e:
449
+ logger.error(f"Error executing net share command: {e}")
450
+ return False
451
+
452
+ def remove_share(share_name: str) -> bool:
453
+ """
454
+ Remove a network share from the local machine.
455
+
456
+ Args:
457
+ share_name: The name of the share to remove.
458
+
459
+ Returns:
460
+ True if the operation was successful, False otherwise.
461
+ """
462
+ if not IS_WINDOWS:
463
+ logger.warning("Network share removal is only available on Windows.")
464
+ return False
465
+
466
+ # Try to use the Windows API if available
467
+ if HAVE_WIN32NET:
468
+ try:
469
+ win32net.NetShareDel(None, share_name)
470
+ logger.info(f"Successfully removed share {share_name}")
471
+ return True
472
+ except Exception as e:
473
+ logger.error(f"Failed to remove share using NetShareDel: {e}")
474
+ # Fall back to net share command
475
+
476
+ # Fall back to using the net share command
477
+ cmd = ['net', 'share', share_name, '/DELETE']
478
+
479
+ try:
480
+ result = subprocess.run(cmd, text=True, capture_output=True, check=False)
481
+
482
+ if result.returncode == 0:
483
+ logger.info(f"Successfully removed share {share_name}")
484
+ return True
485
+ else:
486
+ logger.error(f"Failed to remove share: {result.stderr}")
487
+ return False
488
+ except Exception as e:
489
+ logger.error(f"Error executing net share command: {e}")
490
+ return False