ptdu 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.
- ptdu/__init__.py +38 -0
- ptdu/cache.py +374 -0
- ptdu/errors.py +627 -0
- ptdu/fonts.py +237 -0
- ptdu/main.py +118 -0
- ptdu/models.py +130 -0
- ptdu/performance.py +348 -0
- ptdu/scanner.py +490 -0
- ptdu/threads.py +250 -0
- ptdu/treeview.py +426 -0
- ptdu/ui.py +1247 -0
- ptdu/utils.py +80 -0
- ptdu-0.1.0.dist-info/METADATA +341 -0
- ptdu-0.1.0.dist-info/RECORD +17 -0
- ptdu-0.1.0.dist-info/WHEEL +4 -0
- ptdu-0.1.0.dist-info/entry_points.txt +2 -0
- ptdu-0.1.0.dist-info/licenses/LICENSE +21 -0
ptdu/errors.py
ADDED
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
"""Error handling and user feedback for PTDU."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import tkinter as tk
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from enum import Enum, auto
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from tkinter import messagebox
|
|
11
|
+
from typing import Callable, Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ErrorSeverity(Enum):
|
|
15
|
+
"""Error severity levels."""
|
|
16
|
+
|
|
17
|
+
INFO = auto()
|
|
18
|
+
WARNING = auto()
|
|
19
|
+
ERROR = auto()
|
|
20
|
+
CRITICAL = auto()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class PTDUError:
|
|
25
|
+
"""Structured error information."""
|
|
26
|
+
|
|
27
|
+
message: str
|
|
28
|
+
severity: ErrorSeverity
|
|
29
|
+
path: Optional[Path] = None
|
|
30
|
+
suggestion: Optional[str] = None
|
|
31
|
+
recoverable: bool = True
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ErrorHandler:
|
|
35
|
+
"""Centralized error handling with user-friendly dialogs."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, parent: Optional[tk.Tk] = None) -> None:
|
|
38
|
+
"""Initialize error handler.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
parent: Parent Tk window for dialogs
|
|
42
|
+
"""
|
|
43
|
+
self._parent: Optional[tk.Tk] = parent
|
|
44
|
+
self._error_history: list[PTDUError] = []
|
|
45
|
+
self._max_history: int = 100
|
|
46
|
+
self._silent_mode: bool = False
|
|
47
|
+
|
|
48
|
+
def set_parent(self, parent: tk.Tk) -> None:
|
|
49
|
+
"""Set parent window for dialogs.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
parent: Tk root window
|
|
53
|
+
"""
|
|
54
|
+
self._parent = parent
|
|
55
|
+
|
|
56
|
+
def set_silent_mode(self, silent: bool) -> bool:
|
|
57
|
+
"""Set silent mode (suppress dialogs).
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
silent: True to suppress dialogs
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Previous silent mode state
|
|
64
|
+
"""
|
|
65
|
+
previous = self._silent_mode
|
|
66
|
+
self._silent_mode = silent
|
|
67
|
+
return previous
|
|
68
|
+
|
|
69
|
+
def handle_permission_denied(
|
|
70
|
+
self,
|
|
71
|
+
path: Path,
|
|
72
|
+
operation: str = "access",
|
|
73
|
+
callback: Optional[Callable[[], None]] = None,
|
|
74
|
+
) -> bool:
|
|
75
|
+
"""Handle permission denied error with user dialog.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
path: Path that caused permission error
|
|
79
|
+
operation: Operation being attempted (access, read, delete, etc.)
|
|
80
|
+
callback: Optional callback for retry
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
True if user wants to continue, False to abort
|
|
84
|
+
"""
|
|
85
|
+
error = PTDUError(
|
|
86
|
+
message=f"Permission denied: Cannot {operation} {path.name}",
|
|
87
|
+
severity=ErrorSeverity.ERROR,
|
|
88
|
+
path=path,
|
|
89
|
+
suggestion="Try running with elevated permissions or check file permissions",
|
|
90
|
+
recoverable=True,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
self._log_error(error)
|
|
94
|
+
|
|
95
|
+
if self._silent_mode:
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
# Build dialog message
|
|
99
|
+
msg = f"Permission denied\n\n"
|
|
100
|
+
msg += f"Cannot {operation}:\n{path}\n\n"
|
|
101
|
+
|
|
102
|
+
# Add helpful context
|
|
103
|
+
if path.exists():
|
|
104
|
+
try:
|
|
105
|
+
stat = os.stat(path)
|
|
106
|
+
msg += f"Owner: {stat.st_uid}, Group: {stat.st_gid}\n"
|
|
107
|
+
msg += f"Permissions: {oct(stat.st_mode)[-3:]}\n\n"
|
|
108
|
+
except (OSError, PermissionError):
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
msg += "You may need to run with sudo or check file permissions."
|
|
112
|
+
|
|
113
|
+
# Show dialog with retry option if callback provided
|
|
114
|
+
if callback is not None:
|
|
115
|
+
result = messagebox.askretrycancel(
|
|
116
|
+
"Permission Denied",
|
|
117
|
+
msg,
|
|
118
|
+
icon=messagebox.ERROR,
|
|
119
|
+
parent=self._parent,
|
|
120
|
+
)
|
|
121
|
+
if result:
|
|
122
|
+
callback()
|
|
123
|
+
return True
|
|
124
|
+
return False
|
|
125
|
+
else:
|
|
126
|
+
messagebox.showerror(
|
|
127
|
+
"Permission Denied",
|
|
128
|
+
msg,
|
|
129
|
+
parent=self._parent,
|
|
130
|
+
)
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
def handle_path_too_long(self, path: Path) -> bool:
|
|
134
|
+
"""Handle long path error.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
path: Path that exceeds length limits
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
True if handled, False if critical
|
|
141
|
+
"""
|
|
142
|
+
# Check actual path length
|
|
143
|
+
path_str = str(path)
|
|
144
|
+
path_len = len(path_str)
|
|
145
|
+
|
|
146
|
+
error = PTDUError(
|
|
147
|
+
message=f"Path too long ({path_len} characters)",
|
|
148
|
+
severity=ErrorSeverity.WARNING,
|
|
149
|
+
path=path,
|
|
150
|
+
suggestion="Consider using a shorter path or enabling long path support",
|
|
151
|
+
recoverable=True,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
self._log_error(error)
|
|
155
|
+
|
|
156
|
+
if self._silent_mode:
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
msg = f"Path is very long ({path_len} characters)\n\n"
|
|
160
|
+
msg += f"{path_str[:100]}...\n\n"
|
|
161
|
+
|
|
162
|
+
if path_len > 4096:
|
|
163
|
+
msg += "This path exceeds typical filesystem limits and may cause issues."
|
|
164
|
+
severity = messagebox.ERROR
|
|
165
|
+
else:
|
|
166
|
+
msg += "This path may cause issues on some systems."
|
|
167
|
+
severity = messagebox.WARNING
|
|
168
|
+
|
|
169
|
+
messagebox.showwarning(
|
|
170
|
+
"Long Path Warning",
|
|
171
|
+
msg,
|
|
172
|
+
parent=self._parent,
|
|
173
|
+
)
|
|
174
|
+
return True
|
|
175
|
+
|
|
176
|
+
def handle_scan_error(
|
|
177
|
+
self,
|
|
178
|
+
path: Path,
|
|
179
|
+
error: Exception,
|
|
180
|
+
continue_scan: bool = True,
|
|
181
|
+
) -> bool:
|
|
182
|
+
"""Handle scanning error.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
path: Path being scanned
|
|
186
|
+
error: Exception that occurred
|
|
187
|
+
continue_scan: Whether to offer continue option
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
True to continue, False to abort
|
|
191
|
+
"""
|
|
192
|
+
error_msg = str(error)
|
|
193
|
+
severity = ErrorSeverity.WARNING if continue_scan else ErrorSeverity.ERROR
|
|
194
|
+
|
|
195
|
+
# Categorize common errors
|
|
196
|
+
if "permission" in error_msg.lower():
|
|
197
|
+
return self.handle_permission_denied(path, "scan")
|
|
198
|
+
elif "too long" in error_msg.lower() or len(str(path)) > 4000:
|
|
199
|
+
return self.handle_path_too_long(path)
|
|
200
|
+
|
|
201
|
+
ptdu_error = PTDUError(
|
|
202
|
+
message=f"Scan error: {error_msg}",
|
|
203
|
+
severity=severity,
|
|
204
|
+
path=path,
|
|
205
|
+
suggestion="Check if the path exists and is accessible",
|
|
206
|
+
recoverable=continue_scan,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
self._log_error(ptdu_error)
|
|
210
|
+
|
|
211
|
+
if self._silent_mode:
|
|
212
|
+
return continue_scan
|
|
213
|
+
|
|
214
|
+
msg = f"Error scanning:\n{path}\n\n"
|
|
215
|
+
msg += f"Error: {error_msg}\n\n"
|
|
216
|
+
|
|
217
|
+
if continue_scan:
|
|
218
|
+
msg += "Continue scanning other directories?"
|
|
219
|
+
result = messagebox.askyesno(
|
|
220
|
+
"Scan Error",
|
|
221
|
+
msg,
|
|
222
|
+
icon=messagebox.WARNING,
|
|
223
|
+
parent=self._parent,
|
|
224
|
+
)
|
|
225
|
+
return result
|
|
226
|
+
else:
|
|
227
|
+
messagebox.showerror(
|
|
228
|
+
"Scan Error",
|
|
229
|
+
msg,
|
|
230
|
+
parent=self._parent,
|
|
231
|
+
)
|
|
232
|
+
return False
|
|
233
|
+
|
|
234
|
+
def handle_delete_error(
|
|
235
|
+
self,
|
|
236
|
+
path: Path,
|
|
237
|
+
error: Exception,
|
|
238
|
+
) -> tuple[bool, bool]:
|
|
239
|
+
"""Handle delete operation error.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
path: Path being deleted
|
|
243
|
+
error: Exception that occurred
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
Tuple of (retry, abort_all)
|
|
247
|
+
"""
|
|
248
|
+
error_msg = str(error)
|
|
249
|
+
|
|
250
|
+
if "permission" in error_msg.lower():
|
|
251
|
+
return self._handle_delete_permission_denied(path)
|
|
252
|
+
elif "not empty" in error_msg.lower():
|
|
253
|
+
return self._handle_delete_not_empty(path)
|
|
254
|
+
|
|
255
|
+
ptdu_error = PTDUError(
|
|
256
|
+
message=f"Delete error: {error_msg}",
|
|
257
|
+
severity=ErrorSeverity.ERROR,
|
|
258
|
+
path=path,
|
|
259
|
+
suggestion="Check file permissions and if file is in use",
|
|
260
|
+
recoverable=True,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
self._log_error(ptdu_error)
|
|
264
|
+
|
|
265
|
+
if self._silent_mode:
|
|
266
|
+
return False, False
|
|
267
|
+
|
|
268
|
+
msg = f"Could not delete:\n{path}\n\n"
|
|
269
|
+
msg += f"Error: {error_msg}"
|
|
270
|
+
|
|
271
|
+
result = messagebox.askyesnocancel(
|
|
272
|
+
"Delete Error",
|
|
273
|
+
msg + "\n\nYes = Retry, No = Skip, Cancel = Abort all",
|
|
274
|
+
icon=messagebox.ERROR,
|
|
275
|
+
parent=self._parent,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
if result is True: # Retry
|
|
279
|
+
return True, False
|
|
280
|
+
elif result is False: # Skip
|
|
281
|
+
return False, False
|
|
282
|
+
else: # Cancel
|
|
283
|
+
return False, True
|
|
284
|
+
|
|
285
|
+
def _handle_delete_permission_denied(
|
|
286
|
+
self,
|
|
287
|
+
path: Path,
|
|
288
|
+
) -> tuple[bool, bool]:
|
|
289
|
+
"""Handle delete permission error.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
path: Path being deleted
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Tuple of (retry, abort_all)
|
|
296
|
+
"""
|
|
297
|
+
if self._silent_mode:
|
|
298
|
+
return False, False
|
|
299
|
+
|
|
300
|
+
msg = f"Permission denied when deleting:\n{path}\n\n"
|
|
301
|
+
msg += "You may need elevated permissions to delete this item."
|
|
302
|
+
|
|
303
|
+
result = messagebox.askyesnocancel(
|
|
304
|
+
"Delete Permission Denied",
|
|
305
|
+
msg + "\n\nYes = Retry, No = Skip, Cancel = Abort all",
|
|
306
|
+
icon=messagebox.ERROR,
|
|
307
|
+
parent=self._parent,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
if result is True:
|
|
311
|
+
return True, False
|
|
312
|
+
elif result is False:
|
|
313
|
+
return False, False
|
|
314
|
+
else:
|
|
315
|
+
return False, True
|
|
316
|
+
|
|
317
|
+
def _handle_delete_not_empty(self, path: Path) -> tuple[bool, bool]:
|
|
318
|
+
"""Handle directory not empty error.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
path: Path being deleted
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
Tuple of (retry, abort_all)
|
|
325
|
+
"""
|
|
326
|
+
if self._silent_mode:
|
|
327
|
+
return False, False
|
|
328
|
+
|
|
329
|
+
msg = f"Directory not empty:\n{path}\n\n"
|
|
330
|
+
msg += "The directory contains items that couldn't be removed."
|
|
331
|
+
|
|
332
|
+
result = messagebox.askyesnocancel(
|
|
333
|
+
"Directory Not Empty",
|
|
334
|
+
msg + "\n\nYes = Retry, No = Skip, Cancel = Abort all",
|
|
335
|
+
icon=messagebox.WARNING,
|
|
336
|
+
parent=self._parent,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
if result is True:
|
|
340
|
+
return True, False
|
|
341
|
+
elif result is False:
|
|
342
|
+
return False, False
|
|
343
|
+
else:
|
|
344
|
+
return False, True
|
|
345
|
+
|
|
346
|
+
def handle_export_error(
|
|
347
|
+
self,
|
|
348
|
+
path: Path,
|
|
349
|
+
error: Exception,
|
|
350
|
+
) -> None:
|
|
351
|
+
"""Handle export operation error.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
path: Export path
|
|
355
|
+
error: Exception that occurred
|
|
356
|
+
"""
|
|
357
|
+
error_msg = str(error)
|
|
358
|
+
|
|
359
|
+
ptdu_error = PTDUError(
|
|
360
|
+
message=f"Export error: {error_msg}",
|
|
361
|
+
severity=ErrorSeverity.ERROR,
|
|
362
|
+
path=path,
|
|
363
|
+
suggestion="Check if destination is writable and has available space",
|
|
364
|
+
recoverable=True,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
self._log_error(ptdu_error)
|
|
368
|
+
|
|
369
|
+
if self._silent_mode:
|
|
370
|
+
return
|
|
371
|
+
|
|
372
|
+
msg = f"Could not export to:\n{path}\n\n"
|
|
373
|
+
msg += f"Error: {error_msg}"
|
|
374
|
+
|
|
375
|
+
messagebox.showerror(
|
|
376
|
+
"Export Error",
|
|
377
|
+
msg,
|
|
378
|
+
parent=self._parent,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
def handle_cache_error(
|
|
382
|
+
self,
|
|
383
|
+
error: Exception,
|
|
384
|
+
operation: str = "access",
|
|
385
|
+
) -> None:
|
|
386
|
+
"""Handle cache operation error (non-critical).
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
error: Exception that occurred
|
|
390
|
+
operation: Cache operation being performed
|
|
391
|
+
"""
|
|
392
|
+
# Cache errors are non-critical, just log them
|
|
393
|
+
error_msg = str(error)
|
|
394
|
+
|
|
395
|
+
ptdu_error = PTDUError(
|
|
396
|
+
message=f"Cache {operation} error: {error_msg}",
|
|
397
|
+
severity=ErrorSeverity.WARNING,
|
|
398
|
+
suggestion="Cache will be disabled for this session",
|
|
399
|
+
recoverable=True,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
self._log_error(ptdu_error)
|
|
403
|
+
|
|
404
|
+
# Only show in non-silent mode and if parent exists
|
|
405
|
+
if not self._silent_mode and self._parent is not None:
|
|
406
|
+
# Don't show dialog for cache errors, just log
|
|
407
|
+
pass
|
|
408
|
+
|
|
409
|
+
def show_info(self, title: str, message: str) -> None:
|
|
410
|
+
"""Show info dialog.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
title: Dialog title
|
|
414
|
+
message: Message to display
|
|
415
|
+
"""
|
|
416
|
+
if self._silent_mode:
|
|
417
|
+
return
|
|
418
|
+
|
|
419
|
+
messagebox.showinfo(
|
|
420
|
+
title,
|
|
421
|
+
message,
|
|
422
|
+
parent=self._parent,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
def show_warning(self, title: str, message: str) -> None:
|
|
426
|
+
"""Show warning dialog.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
title: Dialog title
|
|
430
|
+
message: Message to display
|
|
431
|
+
"""
|
|
432
|
+
if self._silent_mode:
|
|
433
|
+
return
|
|
434
|
+
|
|
435
|
+
messagebox.showwarning(
|
|
436
|
+
title,
|
|
437
|
+
message,
|
|
438
|
+
parent=self._parent,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
def show_error(self, title: str, message: str) -> None:
|
|
442
|
+
"""Show error dialog.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
title: Dialog title
|
|
446
|
+
message: Message to display
|
|
447
|
+
"""
|
|
448
|
+
if self._silent_mode:
|
|
449
|
+
return
|
|
450
|
+
|
|
451
|
+
messagebox.showerror(
|
|
452
|
+
title,
|
|
453
|
+
message,
|
|
454
|
+
parent=self._parent,
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
def ask_yes_no(self, title: str, message: str) -> bool:
|
|
458
|
+
"""Ask yes/no question.
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
title: Dialog title
|
|
462
|
+
message: Question to ask
|
|
463
|
+
|
|
464
|
+
Returns:
|
|
465
|
+
True if yes, False if no
|
|
466
|
+
"""
|
|
467
|
+
if self._silent_mode:
|
|
468
|
+
return True
|
|
469
|
+
|
|
470
|
+
return messagebox.askyesno(
|
|
471
|
+
title,
|
|
472
|
+
message,
|
|
473
|
+
parent=self._parent,
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
def _log_error(self, error: PTDUError) -> None:
|
|
477
|
+
"""Log error to history.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
error: Error to log
|
|
481
|
+
"""
|
|
482
|
+
self._error_history.append(error)
|
|
483
|
+
|
|
484
|
+
# Trim history if needed
|
|
485
|
+
while len(self._error_history) > self._max_history:
|
|
486
|
+
self._error_history.pop(0)
|
|
487
|
+
|
|
488
|
+
def get_error_history(
|
|
489
|
+
self,
|
|
490
|
+
severity: Optional[ErrorSeverity] = None,
|
|
491
|
+
) -> list[PTDUError]:
|
|
492
|
+
"""Get error history.
|
|
493
|
+
|
|
494
|
+
Args:
|
|
495
|
+
severity: Filter by severity (None for all)
|
|
496
|
+
|
|
497
|
+
Returns:
|
|
498
|
+
List of errors
|
|
499
|
+
"""
|
|
500
|
+
if severity is None:
|
|
501
|
+
return self._error_history.copy()
|
|
502
|
+
|
|
503
|
+
return [e for e in self._error_history if e.severity == severity]
|
|
504
|
+
|
|
505
|
+
def clear_history(self) -> None:
|
|
506
|
+
"""Clear error history."""
|
|
507
|
+
self._error_history.clear()
|
|
508
|
+
|
|
509
|
+
def get_error_count(self, severity: Optional[ErrorSeverity] = None) -> int:
|
|
510
|
+
"""Get count of errors.
|
|
511
|
+
|
|
512
|
+
Args:
|
|
513
|
+
severity: Filter by severity (None for all)
|
|
514
|
+
|
|
515
|
+
Returns:
|
|
516
|
+
Error count
|
|
517
|
+
"""
|
|
518
|
+
if severity is None:
|
|
519
|
+
return len(self._error_history)
|
|
520
|
+
|
|
521
|
+
return sum(1 for e in self._error_history if e.severity == severity)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
class PathValidator:
|
|
525
|
+
"""Utility for validating and handling paths."""
|
|
526
|
+
|
|
527
|
+
# Maximum safe path length
|
|
528
|
+
MAX_SAFE_PATH_LENGTH: int = 4000
|
|
529
|
+
|
|
530
|
+
@classmethod
|
|
531
|
+
def validate_path(cls, path: Path) -> tuple[bool, Optional[str]]:
|
|
532
|
+
"""Validate a path for operations.
|
|
533
|
+
|
|
534
|
+
Args:
|
|
535
|
+
path: Path to validate
|
|
536
|
+
|
|
537
|
+
Returns:
|
|
538
|
+
Tuple of (is_valid, error_message)
|
|
539
|
+
"""
|
|
540
|
+
path_str = str(path)
|
|
541
|
+
|
|
542
|
+
# Check path length
|
|
543
|
+
if len(path_str) > cls.MAX_SAFE_PATH_LENGTH:
|
|
544
|
+
return (
|
|
545
|
+
False,
|
|
546
|
+
f"Path exceeds maximum length ({len(path_str)} > {cls.MAX_SAFE_PATH_LENGTH})",
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
# Check if path exists
|
|
550
|
+
if not path.exists():
|
|
551
|
+
return False, f"Path does not exist: {path}"
|
|
552
|
+
|
|
553
|
+
# Check for null bytes
|
|
554
|
+
if "\x00" in path_str:
|
|
555
|
+
return False, "Path contains null bytes"
|
|
556
|
+
|
|
557
|
+
return True, None
|
|
558
|
+
|
|
559
|
+
@classmethod
|
|
560
|
+
def sanitize_filename(cls, name: str) -> str:
|
|
561
|
+
"""Sanitize a filename for safe operations.
|
|
562
|
+
|
|
563
|
+
Args:
|
|
564
|
+
name: Filename to sanitize
|
|
565
|
+
|
|
566
|
+
Returns:
|
|
567
|
+
Sanitized filename
|
|
568
|
+
"""
|
|
569
|
+
# Remove or replace unsafe characters
|
|
570
|
+
unsafe_chars = '<>:"/\\|?*\x00-\x1f'
|
|
571
|
+
for char in unsafe_chars:
|
|
572
|
+
name = name.replace(char, "_")
|
|
573
|
+
|
|
574
|
+
# Limit length
|
|
575
|
+
max_len = 255
|
|
576
|
+
if len(name) > max_len:
|
|
577
|
+
name = name[:max_len]
|
|
578
|
+
|
|
579
|
+
return name
|
|
580
|
+
|
|
581
|
+
@classmethod
|
|
582
|
+
def get_path_display_name(cls, path: Path, max_length: int = 60) -> str:
|
|
583
|
+
"""Get display name for a path, truncated if necessary.
|
|
584
|
+
|
|
585
|
+
Args:
|
|
586
|
+
path: Path to display
|
|
587
|
+
max_length: Maximum length
|
|
588
|
+
|
|
589
|
+
Returns:
|
|
590
|
+
Display string
|
|
591
|
+
"""
|
|
592
|
+
path_str = str(path)
|
|
593
|
+
|
|
594
|
+
if len(path_str) <= max_length:
|
|
595
|
+
return path_str
|
|
596
|
+
|
|
597
|
+
# Truncate middle
|
|
598
|
+
prefix_len = max_length // 3
|
|
599
|
+
suffix_len = max_length - prefix_len - 3
|
|
600
|
+
|
|
601
|
+
return f"{path_str[:prefix_len]}...{path_str[-suffix_len:]}"
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
# Global error handler instance
|
|
605
|
+
_global_error_handler: Optional[ErrorHandler] = None
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def get_error_handler() -> ErrorHandler:
|
|
609
|
+
"""Get global error handler instance.
|
|
610
|
+
|
|
611
|
+
Returns:
|
|
612
|
+
ErrorHandler instance
|
|
613
|
+
"""
|
|
614
|
+
global _global_error_handler
|
|
615
|
+
if _global_error_handler is None:
|
|
616
|
+
_global_error_handler = ErrorHandler()
|
|
617
|
+
return _global_error_handler
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def set_error_handler(handler: ErrorHandler) -> None:
|
|
621
|
+
"""Set global error handler.
|
|
622
|
+
|
|
623
|
+
Args:
|
|
624
|
+
handler: ErrorHandler to use
|
|
625
|
+
"""
|
|
626
|
+
global _global_error_handler
|
|
627
|
+
_global_error_handler = handler
|