151 lines
5.8 KiB
Python
151 lines
5.8 KiB
Python
"""
|
|
Step 1: Scan a restored FCCS backup directory for drawer IDs.
|
|
|
|
Every subdirectory in the backup root corresponds to a drawer.
|
|
Writes the sorted list of drawer IDs to the configured drawer_id_file.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
|
|
from fccs_config import (
|
|
parse_args, load_config, make_logger, load_lines, check_for_clashes,
|
|
)
|
|
|
|
|
|
_IGNORE_HEADER = [
|
|
"# Drawer IDs to SKIP during export (one ID per line).",
|
|
"# Lines starting with # are comments and are ignored.",
|
|
"#",
|
|
"# Add password-protected or otherwise-excluded drawers here.",
|
|
"#",
|
|
"# The IDs auto-added below are prefix clashes: searching the base ID in",
|
|
"# FCCS pops a selection box that breaks the plain export. Only the base",
|
|
"# (shorter) ID is listed — the longer, more-specific IDs export normally.",
|
|
"# The base IDs are exported separately by fccs_export_clashes.py, which",
|
|
"# handles the selection box. DELETE or comment out any you'd rather skip.",
|
|
"",
|
|
]
|
|
|
|
|
|
def update_ignore_file(ignore_file, clashes, log):
|
|
"""Create ignore.txt if missing and seed it with clash base (prefix) IDs.
|
|
|
|
Only the shorter (prefix) ID of each clash is added — that's the one that
|
|
triggers the FCCS selection box and needs special handling. The longer,
|
|
more-specific IDs (e.g. '02218A') search fine and export normally.
|
|
Never overwrites existing entries — only appends prefixes not already
|
|
present. Returns the list of IDs added.
|
|
"""
|
|
existing = set(load_lines(ignore_file))
|
|
file_exists = os.path.exists(ignore_file)
|
|
|
|
to_add = [(short, matches) for short, matches in clashes
|
|
if short not in existing]
|
|
|
|
# Existing file already covers every clash — leave it untouched.
|
|
if file_exists and not to_add:
|
|
return []
|
|
|
|
lines = []
|
|
if not file_exists:
|
|
lines.extend(_IGNORE_HEADER)
|
|
for short, matches in to_add:
|
|
lines.append(f"# clashes with: {', '.join(matches)}")
|
|
lines.append(short)
|
|
|
|
mode = "a" if file_exists else "w"
|
|
with open(ignore_file, mode, encoding="utf-8") as f:
|
|
if file_exists:
|
|
f.write("\n") # separate the new block from prior content
|
|
f.write("\n".join(lines) + "\n")
|
|
|
|
added = [short for short, _ in to_add]
|
|
if not file_exists:
|
|
log(f"Created ignore file: {ignore_file}")
|
|
if added:
|
|
log(f"Added {len(added)} clash base ID(s) to ignore list: "
|
|
f"{', '.join(added)}")
|
|
return added
|
|
|
|
|
|
def main():
|
|
args = parse_args()
|
|
cfg = load_config(args.config)
|
|
log = make_logger(cfg.get("paths", "log_file"))
|
|
|
|
backup_dir = cfg.get("paths", "backup_dir")
|
|
output_file = cfg.get("paths", "drawer_id_file")
|
|
ignore_file = cfg.get("paths", "ignore_file")
|
|
|
|
if not os.path.isdir(backup_dir):
|
|
log(f"ERROR: backup directory not found: {backup_dir}")
|
|
sys.exit(1)
|
|
|
|
# Only subdirectories are drawers. When pointed at FCCS's live data dir
|
|
# (e.g. the Restore directory) there are also system folders that start with
|
|
# "$" and miscellaneous loose files — skip both. The isdir check drops the
|
|
# files; the "$" prefix check drops the system folders.
|
|
raw_dirs = [
|
|
name for name in os.listdir(backup_dir)
|
|
if not name.startswith("$")
|
|
and os.path.isdir(os.path.join(backup_dir, name))
|
|
]
|
|
|
|
# FileCabinet CS ignores "." in drawer IDs: a folder named "A123.TJ" on disk
|
|
# is searched and displayed in the UI as "A123TJ", so searching the dotted
|
|
# form returns nothing. Everything downstream uses the UI form — export types
|
|
# the ID into the search box, and FCCS embeds the same dot-free ID as the
|
|
# prefix of exported filenames (which manifest names and verify/report keys
|
|
# are matched against) — so normalize by stripping dots here at the source.
|
|
normalized = {}
|
|
for name in raw_dirs:
|
|
did = name.replace(".", "")
|
|
normalized.setdefault(did, []).append(name)
|
|
drawer_ids = sorted(normalized)
|
|
|
|
with open(output_file, "w", encoding="utf-8") as f:
|
|
for did in drawer_ids:
|
|
f.write(did + "\n")
|
|
|
|
log(f"Found {len(drawer_ids)} drawers, written to {output_file}")
|
|
|
|
# Warn if stripping dots collapsed two distinct folders onto one FCCS ID —
|
|
# that would silently drop a drawer from the inventory otherwise.
|
|
collisions = {did: names for did, names in normalized.items() if len(names) > 1}
|
|
if collisions:
|
|
log("-" * 60)
|
|
log(f"WARNING: {len(collisions)} drawer ID(s) collide after removing "
|
|
"'.' — multiple folders map to a single FCCS ID:")
|
|
for did, names in sorted(collisions.items()):
|
|
log(f" {did} <- {', '.join(sorted(names))}")
|
|
log("-" * 60)
|
|
|
|
# Flag prefix clashes: FCCS search on the shorter ID pops a selection box.
|
|
clashes = check_for_clashes(drawer_ids)
|
|
if clashes:
|
|
log("-" * 60)
|
|
log(f"WARNING: {len(clashes)} potential drawer ID clash(es) found.")
|
|
log("Searching the shorter ID in FCCS may show a selection box "
|
|
"instead of navigating directly, which breaks the export.")
|
|
for short, matches in clashes:
|
|
log(f" {short} -> also matches: {', '.join(matches)}")
|
|
# Seed ignore.txt with the clash prefixes for review (never overwrites).
|
|
update_ignore_file(ignore_file, clashes, log)
|
|
log(f"Review the ignore list: {ignore_file}")
|
|
log("-" * 60)
|
|
else:
|
|
log("No drawer ID prefix clashes found.")
|
|
|
|
# Report which drawers will be skipped by the export.
|
|
ignored = set(load_lines(ignore_file))
|
|
if ignored:
|
|
present = sorted(ignored & set(drawer_ids))
|
|
log(f"Ignore list ({ignore_file}): {len(ignored)} IDs listed, "
|
|
f"{len(present)} present in this backup — these will be SKIPPED "
|
|
f"by fccs_export.py: {', '.join(present) if present else '(none present)'}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|