Subj : showold.py To : Tommi Koivula From : Stephen Walsh Date : Tue Oct 07 2025 21:23:40 Hello Tommi! 07 Oct 25 09:07, you wrote to me: SW>> Do you have the "FIDOCONFIG" environment set? TK> Yes, it works also without any paramaters. But see my previous TK> message. :) Fixed... The script now: 1. Actually does something with the "FIDOCONFIG" environment. 1. Extracts the basename from the configured Outbound path (e.g., "fred" from /home/vk3heg/stats/fred/) 2. Uses that basename instead of hardcoded "outbound" (doh.. I though I'd removed them... ) Try this version. Stephen === Cut === #!/usr/bin/env python3 """ Display outbound summary for every link for which there is anything in the outbound Created by Pavel Gulchouck 2:463/68@fidonet Fixed by Stas Degteff 2:5080/102@fidonet Modified by Michael Dukelsky 2:5020/1042@fidonet Modified by Stephen Walsh 3:633/280@fidonet Python version by Stephen Walsh 3:633/280@fidonet """ import os import sys import glob import re import time from pathlib import Path from typing import Dict, List, Tuple, Optional VERSION = "3.1 # Size constants MB = 1024 * 1024 GB = MB * 1024 def usage(): """Print usage information""" print(""" The script showold.py prints out to STDOUT how much netmail, echomail and files are stored for every link in the outbound and fileboxes and for how long they are stored. If FIDOCONFIG environment variable is defined, you may use the script without arguments, otherwise you have to supply the path to fidoconfig as an argument. Usage: python3 showold.py python3 showold.py Example: python3 showold.py /home/husky/etc/config """) sys.exit(1) def parse_fido_address(addr: str) -> Tuple[int, int, int, int]: """Parse FidoNet address and return sortable tuple. Returns: (zone, net, node, point) """ # Format: zone:net/node[.point][@domain] addr = addr.split('@')[0] # Remove domain zone, net, node, point = 0, 0, 0, 0 if ':' in addr: zone_part, rest = addr.split(':', 1) zone = int(zone_part) if zone_part.isdigit() else 0 else: rest = addr if '/' in rest: net_part, node_part = rest.split('/', 1) net = int(net_part) if net_part.isdigit() else 0 if '.' in node_part: node_str, point_str = node_part.split('.', 1) node = int(node_str) if node_str.isdigit() else 0 point = int(point_str) if point_str.isdigit() else 0 else: node = int(node_part) if node_part.isdigit() else 0 return (zone, net, node, point) def node_sort_key(addr: str) -> Tuple[int, int, int, int]: """Return sortable key for FidoNet address""" return parse_fido_address(addr) def unbso(filename: str, directory: str, def_zone: int, outbound_basename: str = 'outbound') -> str: """Parse BSO filename and directory to get FidoNet address""" # Check if we're in a point directory dir_name = os.path.basename(directory) is_point_dir = False net = None node = None # Match point directory: NNNNPPPP.pnt pnt_match = re.match(r'^([0-9a-f]{4})([0-9a-f]{4})\.pnt$', dir_name, re.I) if pnt_match: net = int(pnt_match.group(1), 16) node = int(pnt_match.group(2), 16) is_point_dir = True directory = os.path.dirname(directory) # Determine zone from directory name dir_name = os.path.basename(directory) zone_match = re.match(rf'^{re.escape(outbound_basename)}\.([0-9a-f]{{3}})$', dir_name, re.I) if zone_match: zone = int(zone_match.group(1), 16) elif dir_name.lower() == outbound_basename.lower(): zone = def_zone else: zone = def_zone # Parse filename if is_point_dir: # In point directory: filename is 8 hex digits (point number) file_match = re.match(r'^([0-9a-f]{8})', filename, re.I) if file_match and net is not None and node is not None: point = int(file_match.group(1), 16) return f"{zone}:{net}/{node}.{point}" else: # Not in point directory: NNNNPPPP format file_match = re.match(r'^([0-9a-f]{4})([0-9a-f]{4})', filename, re.I) if file_match: net = int(file_match.group(1), 16) node = int(file_match.group(2), 16) return f"{zone}:{net}/{node}" return "" def unaso(filename: str) -> str: """Parse ASO filename to get FidoNet address""" match = re.match(r'(\d+)\.(\d+)\.(\d+)\.(\d+)', filename) if match: zone, net, node, point = match.groups() if point == '0': return f"{zone}:{net}/{node}" else: return f"{zone}:{net}/{node}.{point}" return "" def unbox(directory: str) -> str: """Parse filebox directory name to get FidoNet address""" dir_name = os.path.basename(directory.rstrip('/')) match = re.match(r'(\d+)\.(\d+)\.(\d+)\.(\d+)(?:\.h)?$', dir_name, re.I) if match: zone, net, node, point = match.groups() if point == '0': return f"{zone}:{net}/{node}" else: return f"{zone}:{net}/{node}.{point}" return "" def nice_number(num: int) -> float: """Convert number to nice format (MB or GB)""" if num < MB: return num elif num >= MB and num < GB: return num / MB else: return num / GB def nice_number_format(num: int) -> str: """Return format string for nice number""" if num < MB: return f"{num:9d} " elif num < GB: return f"{num/MB:9.4f}M" else: return f"{num/GB:9.4f}G" def find_outbounds(base_dir: str, outbound_basename: str = 'outbound') -> List[str]: """Find all outbound directories""" outbounds = [] for root, dirs, files in os.walk(base_dir): for d in dirs: if re.match(rf'^{re.escape(outbound_basename)}(?:\.[0-9a-f]{{3}})?$', d, re.I): outbounds.append(os.path.join(root, d)) return outbounds def find_fileboxes(base_dir: str) -> List[str]: """Find all filebox directories""" boxes = [] for root, dirs, files in os.walk(base_dir): for d in dirs: if re.match(r'\d+\.\d+\.\d+\.\d+(?:\.h)?$', d, re.I): boxes.append(os.path.join(root, d)) return boxes def read_fidoconfig(config_path: str) -> Dict[str, str]: """Read fidoconfig file and extract needed values""" config = {} with open(config_path, 'r', encoding='utf-8', errors='replace') as f: for line in f: line = line.strip() if not line or line.startswith('#'): continue # Simple tokenization parts = line.split(None, 1) if len(parts) < 2: continue keyword = parts[0].lower() value = parts[1] if keyword == 'address' and 'address' not in config: config['address'] = value elif keyword == 'outbound': config['outbound'] = value.rstrip('/') elif keyword == 'fileboxesdir': config['fileboxesdir'] = value.rstrip('/') elif keyword == 'passfileareadir': config['passfileareadir'] = value.rstrip('/') return config def process_bso_outbound(outbound_dir: str, def_zone: int, pass_file_area_dir: str, minmtime: Dict, netmail: Dict, echomail: Dict, files: Dict, outbound_basename: str = 'outbound'): """Process BSO outbound directory""" # Find all control files in outbound control_patterns = ['*.[IiCcDdFfHh][Ll][Oo]', '*.[IiCcDdOoHh][Uu][Tt]'] control_files = [] for pattern in control_patterns: control_files.extend( glob.glob(os.path.join(outbound_dir, pattern))) # Find point directories pnt_dirs = glob.glob(os.path.join(outbound_dir, '*.[Pp][Nn][Tt]')) for pnt_dir in pnt_dirs: if os.path.isdir(pnt_dir): for pattern in control_patterns: control_files.extend( glob.glob(os.path.join(pnt_dir, pattern))) for ctrl_file in control_files: directory = os.path.dirname(ctrl_file) filename = os.path.basename(ctrl_file) node = unbso(filename, directory, def_zone, outbound_basename) if not node: continue stat_info = os.stat(ctrl_file) size = stat_info.st_size mtime = stat_info.st_mtime if size == 0: continue # Update min mtime if node not in minmtime or mtime < minmtime[node]: minmtime[node] = mtime # Check if it's netmail if ctrl_file.lower().endswith('ut'): netmail[node] = netmail.get(node, 0) + size continue # Process control file contents is_echomail_ctrl = bool(re.search(r'\.(c|h|f)lo$', ctrl_file, re.I)) is_file_ctrl = bool(re.search(r'\.(c|i|d)lo$', ctrl_file, re.I)) try: with open(ctrl_file, 'r', encoding='utf-8', errors='replace') as f: for line in f: line = line.strip() line = re.sub(r'^[#~^]', '', line) if not line: continue bundle_path = None # Check if absolute path if line.startswith('/'): bundle_path = line # Check if it's in passFileAreaDir elif (pass_file_area_dir and line.startswith(pass_file_area_dir)): bundle_path = line # Check for bundle patterns elif re.match( r'^[0-9a-f]{8}\.(su|mo|tu|we|th|fr|sa)[0-9a-z]$', line, re.I): bundle_path = os.path.join(directory, line) elif re.match(r'\.tic$', line, re.I): bundle_path = os.path.join(directory, line) elif re.match(r'^[0-9a-f]{8}\.pkt$', line, re.I): bundle_path = os.path.join(directory, line) if bundle_path and os.path.exists(bundle_path): b_stat = os.stat(bundle_path) b_size = b_stat.st_size b_mtime = b_stat.st_mtime if (node not in minmtime or b_mtime < minmtime[node]): minmtime[node] = b_mtime # Categorize if re.search( r'\.(su|mo|tu|we|th|fr|sa)[0-9a-z]$', line, re.I): echomail[node] = echomail.get(node, 0) + b_size elif (bundle_path.endswith('.pkt') and is_echomail_ctrl): echomail[node] = echomail.get(node, 0) + b_size elif bundle_path.endswith('.tic'): files[node] = files.get(node, 0) + b_size elif (pass_file_area_dir and bundle_path.startswith(pass_file_area_dir)): files[node] = files.get(node, 0) + b_size elif line.startswith('/') and is_file_ctrl: files[node] = files.get(node, 0) + b_size elif line.startswith('/'): files[node] = files.get(node, 0) + b_size except Exception as e: print(f"WARN: Could not process {ctrl_file}: {e}", file=sys.stderr) def process_fileboxes(box_dir: str, minmtime: Dict, netmail: Dict, echomail: Dict, files: Dict): """Process filebox directory""" node = unbox(box_dir) if not node: return # Find all files in the filebox patterns = ['*.[IiCcDdOoHh][Uu][Tt]', '*.[Ss][Uu][0-9a-zA-Z]', '*.[Mm][Oo][0-9a-zA-Z]', '*.[Tt][Uu][0-9a-zA-Z]', '*.[Ww][Ee][0-9a-zA-Z]', '*.[Tt][Hh][0-9a-zA-Z]', '*.[Ff][Rr][0-9a-zA-Z]', '*.[Ss][Aa][0-9a-zA-Z]'] file_list = [] for pattern in patterns: file_list.extend(glob.glob(os.path.join(box_dir, pattern))) for fpath in file_list: if not os.path.isfile(fpath): continue stat_info = os.stat(fpath) size = stat_info.st_size mtime = stat_info.st_mtime if size == 0: continue if node not in minmtime or mtime < minmtime[node]: minmtime[node] = mtime filename = os.path.basename(fpath) if re.search(r'ut$', filename, re.I): netmail[node] = netmail.get(node, 0) + size elif re.search(r'\.(su|mo|tu|we|th|fr|sa)[0-9a-z]$', filename, re.I): echomail[node] = echomail.get(node, 0) + size else: files[node] = files.get(node, 0) + size def main(): # Get fidoconfig path fidoconfig = os.environ.get('FIDOCONFIG') if len(sys.argv) == 2: if sys.argv[1] in ['-h', '--help', '-?', '/?', '/h']: usage() fidoconfig = sys.argv[1] elif not fidoconfig: usage() if not os.path.isfile(fidoconfig): print(f"\n'{fidoconfig}' is not a fidoconfig file\n") usage() # Read config print(f"Showold.py version '{VERSION}'") config = read_fidoconfig(fidoconfig) if 'address' not in config: print("\nYour FTN address is not defined\n", file=sys.stderr) sys.exit(1) # Parse default zone addr_match = re.match(r'^(\d+):\d+/\d+', config['address']) if not addr_match: print("\nYour FTN address has a syntax error\n", file=sys.stderr) sys.exit(1) def_zone = int(addr_match.group(1)) if 'outbound' not in config: print("\nOutbound is not defined\n", file=sys.stderr) sys.exit(1) outbound = config['outbound'] if not os.path.isdir(outbound): print(f"\nOutbound '{outbound}' is not a directory\n", file=sys.stderr) sys.exit(1) # Get parent directory for finding all outbounds husky_base_dir = os.path.dirname(outbound) outbound_basename = os.path.basename(outbound) print(f"Searching for outbounds in: '{husky_base_dir}'") # Find directories outbound_dirs = find_outbounds(husky_base_dir, outbound_basename) fileboxes_dir = config.get('fileboxesdir', '') filebox_dirs = [] if fileboxes_dir and os.path.isdir(fileboxes_dir): filebox_dirs = find_fileboxes(fileboxes_dir) pass_file_area_dir = config.get('passfileareadir', '') # Process all outbounds minmtime = {} netmail = {} echomail = {} files_dict = {} for ob_dir in outbound_dirs: process_bso_outbound(ob_dir, def_zone, pass_file_area_dir, minmtime, netmail, echomail, files_dict, outbound_basename) for fb_dir in filebox_dirs: process_fileboxes(fb_dir, minmtime, netmail, echomail, files_dict) # Print results print("+------------------+--------+-----------+-----------+" "-----------+") print("| Node | Days | NetMail | EchoMail " "| Files |") print("+------------------+--------+-----------+-----------+" "-----------+") for node in sorted(minmtime.keys(), key=node_sort_key): nm = netmail.get(node, 0) em = echomail.get(node, 0) fl = files_dict.get(node, 0) days = (time.time() - minmtime[node]) / (24 * 60 * 60) print(f"| {node:16s} |{days:7.0f} |" f"{nice_number_format(nm)} |" f"{nice_number_format(em)} |" f"{nice_number_format(fl)} |") print("+------------------+--------+-----------+-----------+" "-----------+") if __name__ == '__main__': main() === Cut === --- GoldED+/LNX 1.1.5-b20250409 * Origin: Dragon's Lair ---:- dragon.vk3heg.net -:--- Prt: 6800 (3:633/280) .