Hello Tommi!
07 Oct 25 09:07, you wrote to me:
Do you have the "FIDOCONFIG" environment set?
Yes, it works also without any paramaters. But see my previous
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 <path to fidoconfig>
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)