@@ -31,13 +31,15 @@ class MountCheckResult:
3131 var_name : str
3232 path : str = ""
3333 is_writeable : bool = False
34+ is_readable : bool = False
3435 is_mounted : bool = False
3536 is_mount_point : bool = False
3637 is_ramdisk : bool = False
3738 underlying_fs_is_ramdisk : bool = False # Track this separately
3839 fstype : str = "N/A"
3940 error : bool = False
4041 write_error : bool = False
42+ read_error : bool = False
4143 performance_issue : bool = False
4244 dataloss_risk : bool = False
4345 category : str = ""
@@ -97,7 +99,7 @@ def _resolve_writeable_state(target_path: str) -> bool:
9799 if os .path .exists (current ):
98100 if not os .access (current , os .W_OK ):
99101 return False
100-
102+
101103 # OverlayFS/Copy-up check: Try to actually write a file to verify
102104 if os .path .isdir (current ):
103105 test_file = os .path .join (current , f".netalertx_write_test_{ os .getpid ()} " )
@@ -108,7 +110,7 @@ def _resolve_writeable_state(target_path: str) -> bool:
108110 return True
109111 except OSError :
110112 return False
111-
113+
112114 return True
113115
114116 parent_dir = os .path .dirname (current )
@@ -119,6 +121,27 @@ def _resolve_writeable_state(target_path: str) -> bool:
119121 return False
120122
121123
124+ def _resolve_readable_state (target_path : str ) -> bool :
125+ """Determine if a path is readable, ascending to the first existing parent."""
126+
127+ current = target_path
128+ seen : set [str ] = set ()
129+ while True :
130+ if current in seen :
131+ break
132+ seen .add (current )
133+
134+ if os .path .exists (current ):
135+ return os .access (current , os .R_OK )
136+
137+ parent_dir = os .path .dirname (current )
138+ if not parent_dir or parent_dir == current :
139+ break
140+ current = parent_dir
141+
142+ return False
143+
144+
122145def analyze_path (
123146 spec : PathSpec ,
124147 mounted_filesystems ,
@@ -142,14 +165,20 @@ def analyze_path(
142165
143166 result .path = target_path
144167
145- # --- 1. Check Write Permissions ---
168+ # --- 1. Check Read/ Write Permissions ---
146169 result .is_writeable = _resolve_writeable_state (target_path )
170+ result .is_readable = _resolve_readable_state (target_path )
147171
148172 if not result .is_writeable :
149173 result .error = True
150174 if spec .role != "secondary" :
151175 result .write_error = True
152176
177+ if not result .is_readable :
178+ result .error = True
179+ if spec .role != "secondary" :
180+ result .read_error = True
181+
153182 # --- 2. Check Filesystem Type (Parent and Self) ---
154183 parent_mount_fstype = ""
155184 longest_mount = ""
@@ -184,6 +213,8 @@ def analyze_path(
184213 result .is_ramdisk = parent_mount_fstype in non_persistent_fstypes
185214
186215 # --- 4. Apply Risk Logic ---
216+ # Keep risk flags about persistence/performance properties of the mount itself.
217+ # Read/write permission problems are surfaced via the R/W columns and error flags.
187218 if spec .category == "persist" :
188219 if result .underlying_fs_is_ramdisk or result .is_ramdisk :
189220 result .dataloss_risk = True
@@ -198,25 +229,40 @@ def analyze_path(
198229 return result
199230
200231
201- def print_warning_message ():
232+ def print_warning_message (results : list [ MountCheckResult ] ):
202233 """Prints a formatted warning to stderr."""
203234 YELLOW = "\033 [1;33m"
204235 RESET = "\033 [0m"
205236
237+ print (f"{ YELLOW } ══════════════════════════════════════════════════════════════════════════════" , file = sys .stderr )
238+ print ("⚠️ ATTENTION: Configuration issues detected (marked with ❌).\n " , file = sys .stderr )
239+
240+ for r in results :
241+ issues = []
242+ if not r .is_writeable :
243+ issues .append ("error writing" )
244+ if not r .is_readable :
245+ issues .append ("error reading" )
246+ if not r .is_mounted and (r .category == "persist" or r .category == "ramdisk" ):
247+ issues .append ("not mounted" )
248+ if r .dataloss_risk :
249+ issues .append ("risk of dataloss" )
250+ if r .performance_issue :
251+ issues .append ("performance issue" )
252+
253+ if issues :
254+ print (f" * { r .path } { ', ' .join (issues )} " , file = sys .stderr )
255+
206256 message = (
207- "══════════════════════════════════════════════════════════════════════════════\n "
208- "⚠️ ATTENTION: Configuration issues detected (marked with ❌).\n \n "
209- " Your configuration has write permission, dataloss, or performance issues\n "
210- " as shown in the table above.\n \n "
211- " We recommend starting with the default docker-compose.yml as the\n "
257+ "\n We recommend starting with the default docker-compose.yml as the\n "
212258 " configuration can be quite complex.\n \n "
213259 " Review the documentation for a correct setup:\n "
214260 " https://github.com/jokob-sk/NetAlertX/blob/main/docs/DOCKER_COMPOSE.md\n "
215261 " https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/mount-configuration-issues.md\n "
216262 "══════════════════════════════════════════════════════════════════════════════\n "
217263 )
218264
219- print (f"{ YELLOW } { message } { RESET } " , file = sys .stderr )
265+ print (f"{ message } { RESET } " , file = sys .stderr )
220266
221267
222268def _get_active_specs () -> list [PathSpec ]:
@@ -231,14 +277,14 @@ def _sub_result_is_healthy(result: MountCheckResult) -> bool:
231277 if result .category == "persist" :
232278 if not result .is_mounted :
233279 return False
234- if result .dataloss_risk or result .write_error or result .error :
280+ if result .dataloss_risk or result .write_error or result .read_error or result . error :
235281 return False
236282 return True
237283
238284 if result .category == "ramdisk" :
239285 if not result .is_mounted or not result .is_ramdisk :
240286 return False
241- if result .performance_issue or result .write_error or result .error :
287+ if result .performance_issue or result .write_error or result .read_error or result . error :
242288 return False
243289 return True
244290
@@ -278,19 +324,9 @@ def _apply_primary_rules(specs: list[PathSpec], results_map: dict[str, MountChec
278324 )
279325 all_core_subs_are_mounts = bool (core_sub_results ) and len (core_mount_points ) == len (core_sub_results )
280326
281- if all_core_subs_healthy :
282- if result .write_error :
283- result .write_error = False
284- if not result .is_writeable :
285- result .is_writeable = True
286- if spec .category == "persist" and result .dataloss_risk :
287- result .dataloss_risk = False
288- if result .error and not (result .performance_issue or result .dataloss_risk or result .write_error ):
289- result .error = False
290-
291327 suppress_primary = False
292328 if all_core_subs_healthy and all_core_subs_are_mounts :
293- if not result .is_mount_point and not result .error and not result .write_error :
329+ if not result .is_mount_point and not result .error and not result .write_error and not result . read_error :
294330 suppress_primary = True
295331
296332 if suppress_primary :
@@ -329,14 +365,14 @@ def main():
329365 results = _apply_primary_rules (active_specs , results_map )
330366
331367 has_issues = any (
332- r .dataloss_risk or r .error or r .write_error or r .performance_issue
368+ r .dataloss_risk or r .error or r .write_error or r .read_error or r . performance_issue
333369 for r in results
334370 )
335- has_write_errors = any (r .write_error for r in results )
371+ has_rw_errors = any (r .write_error or r . read_error for r in results )
336372
337373 if has_issues or True : # Always print table for diagnostic purposes
338374 # --- Print Table ---
339- headers = ["Path" , "Writeable " , "Mount" , "RAMDisk" , "Performance" , "DataLoss" ]
375+ headers = ["Path" , "R" , "W " , "Mount" , "RAMDisk" , "Performance" , "DataLoss" ]
340376
341377 CHECK_SYMBOL = "✅"
342378 CROSS_SYMBOL = "❌"
@@ -355,7 +391,8 @@ def bool_to_check(is_good):
355391 f" {{:^{ col_widths [2 ]} }} |"
356392 f" {{:^{ col_widths [3 ]} }} |"
357393 f" {{:^{ col_widths [4 ]} }} |"
358- f" {{:^{ col_widths [5 ]} }} "
394+ f" {{:^{ col_widths [5 ]} }} |"
395+ f" {{:^{ col_widths [6 ]} }} "
359396 )
360397
361398 row_fmt = (
@@ -364,7 +401,8 @@ def bool_to_check(is_good):
364401 f" {{:^{ col_widths [2 ]} }}|" # No space
365402 f" {{:^{ col_widths [3 ]} }}|" # No space
366403 f" {{:^{ col_widths [4 ]} }}|" # No space
367- f" {{:^{ col_widths [5 ]} }} " # DataLoss is last, needs space
404+ f" {{:^{ col_widths [5 ]} }}|" # No space
405+ f" {{:^{ col_widths [6 ]} }} " # DataLoss is last, needs space
368406 )
369407
370408 separator = "" .join ([
@@ -378,13 +416,16 @@ def bool_to_check(is_good):
378416 "+" ,
379417 "-" * (col_widths [4 ] + 2 ),
380418 "+" ,
381- "-" * (col_widths [5 ] + 2 )
419+ "-" * (col_widths [5 ] + 2 ),
420+ "+" ,
421+ "-" * (col_widths [6 ] + 2 )
382422 ])
383423
384- print (header_fmt .format (* headers ))
385- print (separator )
424+ print (header_fmt .format (* headers ), file = sys . stderr )
425+ print (separator , file = sys . stderr )
386426 for r in results :
387427 # Symbol Logic
428+ read_symbol = bool_to_check (r .is_readable )
388429 write_symbol = bool_to_check (r .is_writeable )
389430
390431 mount_symbol = CHECK_SYMBOL if r .is_mounted else CROSS_SYMBOL
@@ -407,21 +448,23 @@ def bool_to_check(is_good):
407448 print (
408449 row_fmt .format (
409450 r .path ,
451+ read_symbol ,
410452 write_symbol ,
411453 mount_symbol ,
412454 ramdisk_symbol ,
413455 perf_symbol ,
414456 dataloss_symbol ,
415- )
457+ ),
458+ file = sys .stderr
416459 )
417460
418461 # --- Print Warning ---
419462 if has_issues :
420463 print ("\n " , file = sys .stderr )
421- print_warning_message ()
464+ print_warning_message (results )
422465
423- # Exit with error only if there are write permission issues
424- if has_write_errors and os .environ .get ("NETALERTX_DEBUG" ) != "1" :
466+ # Exit with error only if there are read/ write permission issues
467+ if has_rw_errors and os .environ .get ("NETALERTX_DEBUG" ) != "1" :
425468 sys .exit (1 )
426469
427470
0 commit comments