had to error handle the reog script to add safe handling of weird folder paths with odd characaters

This commit is contained in:
2026-07-21 07:24:50 -05:00
parent 9116d1c04a
commit 07472711e7
3 changed files with 73 additions and 38 deletions

View File

@@ -94,6 +94,8 @@ Exported filenames follow the format `{drawer_id}_{client_name}_{folder_name}_{c
Files that can't be fully parsed still keep their client: the drawer ID and client name are the first two underscore-delimited tokens and stay recoverable even when the folder/date parse fails, so those files are filed under `{client_name}/_unparsed/` (retaining their original filename). Only files whose client can't be recovered at all fall back to the top-level `_unparsed/`. Files that can't be fully parsed still keep their client: the drawer ID and client name are the first two underscore-delimited tokens and stay recoverable even when the folder/date parse fails, so those files are filed under `{client_name}/_unparsed/` (retaining their original filename). Only files whose client can't be recovered at all fall back to the top-level `_unparsed/`.
Folder names are sanitized for Windows before use — trailing spaces and periods are stripped from each path component, since Windows can't create a directory ending in a space or dot (e.g. a client named `FARR GROUP, P.L.` becomes `FARR GROUP, P.L`). Reorganization is also resilient per file: if one file can't be placed for any reason, the error is logged and counted (reported as `errored` in the summary) and the run continues with the rest rather than aborting.
### Step 4 (optional): Verify Export Completeness ### Step 4 (optional): Verify Export Completeness
During export, each drawer's document list is captured from the FCCS dialog and saved as a manifest (in `manifest_dir`). These tools compare the manifests against the files actually in the export folder to confirm nothing was missed. During export, each drawer's document list is captured from the FCCS dialog and saved as a manifest (in `manifest_dir`). These tools compare the manifests against the files actually in the export folder to confirm nothing was missed.

View File

@@ -97,6 +97,19 @@ def parse_filename(filename, folder_patterns):
return None return None
def safe_component(name):
"""Make a single path component safe to create on Windows.
Windows cannot create a directory whose name ends in a space or a period —
os.makedirs raises FileNotFoundError (WinError 3). Client names such as
'FARR GROUP, P.L.' (trailing period) or ones with a stray trailing space
trigger this, so strip trailing dots/spaces. Falls back to '_' if the name
is entirely dots/spaces.
"""
cleaned = name.rstrip(" .")
return cleaned if cleaned else "_"
def main(): def main():
args = parse_args() args = parse_args()
cfg = load_config(args.config) cfg = load_config(args.config)
@@ -125,36 +138,42 @@ def main():
success = 0 success = 0
failed = 0 failed = 0
errored = 0
for filename in files: for filename in files:
# Defensive per-file: a single bad path (e.g. an un-creatable name) must
# not abort the whole run — log it and move on to the next file.
try:
result = parse_filename(filename, folder_patterns) result = parse_filename(filename, folder_patterns)
if result is None: if result is None:
# Still recover the client from the leading tokens so the file lands # Still recover the client from the leading tokens so the file
# in that client's folder (in an _unparsed subfolder to flag it), # lands in that client's folder (in an _unparsed subfolder to
# rather than a single top-level bucket. Fall back to the top-level # flag it), rather than a single top-level bucket. Fall back to
# _unparsed only when even the client can't be determined. # the top-level _unparsed only when even the client is unknown.
client_name = client_name_from_filename(filename) client_name = client_name_from_filename(filename)
if client_name: if client_name:
dest_dir = os.path.join(output_dir, client_name, "_unparsed") dest_dir = os.path.join(output_dir,
safe_component(client_name),
"_unparsed")
log(f" UNPARSED (filed under {client_name}): {filename}") log(f" UNPARSED (filed under {client_name}): {filename}")
else: else:
dest_dir = os.path.join(output_dir, "_unparsed") dest_dir = os.path.join(output_dir, "_unparsed")
log(f" UNPARSED (no client): {filename}") log(f" UNPARSED (no client): {filename}")
failed += 1 dest_filename = filename # keep original name for review
parsed = False
else: else:
drawer_id, client_name, folder_parts, date, doc_name = result drawer_id, client_name, folder_parts, date, doc_name = result
dest_dir = os.path.join( dest_dir = os.path.join(
output_dir, output_dir,
client_name, safe_component(client_name),
*folder_parts, *(safe_component(p) for p in folder_parts),
) )
success += 1 dest_filename = doc_name
parsed = True
os.makedirs(dest_dir, exist_ok=True) os.makedirs(dest_dir, exist_ok=True)
src = os.path.join(export_dir, filename) src = os.path.join(export_dir, filename)
# For parsed files, use just the document name; for unparsed, keep original
dest_filename = doc_name if result else filename
dst = os.path.join(dest_dir, dest_filename) dst = os.path.join(dest_dir, dest_filename)
# Handle duplicate filenames # Handle duplicate filenames
@@ -167,13 +186,27 @@ def main():
log(f" DUPLICATE renamed: {dest_filename} -> {os.path.basename(dst)}") log(f" DUPLICATE renamed: {dest_filename} -> {os.path.basename(dst)}")
shutil.copy2(src, dst) shutil.copy2(src, dst)
except Exception as e:
log(f" ERROR processing {filename}: {e}")
errored += 1
continue
# Count only after a successful copy so the totals stay accurate.
if parsed:
success += 1
else:
failed += 1
log("=" * 60) log("=" * 60)
log(f"Reorganization complete: {success} organized, {failed} unparsed") log(f"Reorganization complete: {success} organized, {failed} unparsed, "
f"{errored} errored")
if failed: if failed:
log("Review unparsed files in each client's _unparsed subfolder " log("Review unparsed files in each client's _unparsed subfolder "
f"(and {os.path.join(output_dir, '_unparsed')} for any without a " f"(and {os.path.join(output_dir, '_unparsed')} for any without a "
"detectable client).") "detectable client).")
if errored:
log(f"{errored} file(s) could not be placed due to errors — see the "
"ERROR lines above; these were left in the export folder.")
log("=" * 60) log("=" * 60)