had to error handle the reog script to add safe handling of weird folder paths with odd characaters
This commit is contained in:
@@ -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/`.
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
Binary file not shown.
@@ -97,6 +97,19 @@ def parse_filename(filename, folder_patterns):
|
||||
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():
|
||||
args = parse_args()
|
||||
cfg = load_config(args.config)
|
||||
@@ -125,55 +138,75 @@ def main():
|
||||
|
||||
success = 0
|
||||
failed = 0
|
||||
errored = 0
|
||||
|
||||
for filename in files:
|
||||
result = parse_filename(filename, folder_patterns)
|
||||
# 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)
|
||||
|
||||
if result is None:
|
||||
# Still recover the client from the leading tokens so the file lands
|
||||
# in that client's folder (in an _unparsed subfolder to flag it),
|
||||
# rather than a single top-level bucket. Fall back to the top-level
|
||||
# _unparsed only when even the client can't be determined.
|
||||
client_name = client_name_from_filename(filename)
|
||||
if client_name:
|
||||
dest_dir = os.path.join(output_dir, client_name, "_unparsed")
|
||||
log(f" UNPARSED (filed under {client_name}): {filename}")
|
||||
if result is None:
|
||||
# Still recover the client from the leading tokens so the file
|
||||
# lands in that client's folder (in an _unparsed subfolder to
|
||||
# flag it), rather than a single top-level bucket. Fall back to
|
||||
# the top-level _unparsed only when even the client is unknown.
|
||||
client_name = client_name_from_filename(filename)
|
||||
if client_name:
|
||||
dest_dir = os.path.join(output_dir,
|
||||
safe_component(client_name),
|
||||
"_unparsed")
|
||||
log(f" UNPARSED (filed under {client_name}): {filename}")
|
||||
else:
|
||||
dest_dir = os.path.join(output_dir, "_unparsed")
|
||||
log(f" UNPARSED (no client): {filename}")
|
||||
dest_filename = filename # keep original name for review
|
||||
parsed = False
|
||||
else:
|
||||
dest_dir = os.path.join(output_dir, "_unparsed")
|
||||
log(f" UNPARSED (no client): {filename}")
|
||||
failed += 1
|
||||
else:
|
||||
drawer_id, client_name, folder_parts, date, doc_name = result
|
||||
dest_dir = os.path.join(
|
||||
output_dir,
|
||||
client_name,
|
||||
*folder_parts,
|
||||
)
|
||||
drawer_id, client_name, folder_parts, date, doc_name = result
|
||||
dest_dir = os.path.join(
|
||||
output_dir,
|
||||
safe_component(client_name),
|
||||
*(safe_component(p) for p in folder_parts),
|
||||
)
|
||||
dest_filename = doc_name
|
||||
parsed = True
|
||||
|
||||
os.makedirs(dest_dir, exist_ok=True)
|
||||
src = os.path.join(export_dir, filename)
|
||||
dst = os.path.join(dest_dir, dest_filename)
|
||||
|
||||
# Handle duplicate filenames
|
||||
if os.path.exists(dst):
|
||||
base, fext = os.path.splitext(dest_filename)
|
||||
counter = 1
|
||||
while os.path.exists(dst):
|
||||
dst = os.path.join(dest_dir, f"{base}_{counter}{fext}")
|
||||
counter += 1
|
||||
log(f" DUPLICATE renamed: {dest_filename} -> {os.path.basename(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
|
||||
|
||||
os.makedirs(dest_dir, exist_ok=True)
|
||||
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)
|
||||
|
||||
# Handle duplicate filenames
|
||||
if os.path.exists(dst):
|
||||
base, fext = os.path.splitext(dest_filename)
|
||||
counter = 1
|
||||
while os.path.exists(dst):
|
||||
dst = os.path.join(dest_dir, f"{base}_{counter}{fext}")
|
||||
counter += 1
|
||||
log(f" DUPLICATE renamed: {dest_filename} -> {os.path.basename(dst)}")
|
||||
|
||||
shutil.copy2(src, dst)
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
log("=" * 60)
|
||||
log(f"Reorganization complete: {success} organized, {failed} unparsed")
|
||||
log(f"Reorganization complete: {success} organized, {failed} unparsed, "
|
||||
f"{errored} errored")
|
||||
if failed:
|
||||
log("Review unparsed files in each client's _unparsed subfolder "
|
||||
f"(and {os.path.join(output_dir, '_unparsed')} for any without a "
|
||||
"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)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user