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/`.
|
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.
|
||||||
|
|||||||
Binary file not shown.
@@ -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,55 +138,75 @@ def main():
|
|||||||
|
|
||||||
success = 0
|
success = 0
|
||||||
failed = 0
|
failed = 0
|
||||||
|
errored = 0
|
||||||
|
|
||||||
for filename in files:
|
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:
|
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,
|
||||||
log(f" UNPARSED (filed under {client_name}): {filename}")
|
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:
|
else:
|
||||||
dest_dir = os.path.join(output_dir, "_unparsed")
|
drawer_id, client_name, folder_parts, date, doc_name = result
|
||||||
log(f" UNPARSED (no client): {filename}")
|
dest_dir = os.path.join(
|
||||||
failed += 1
|
output_dir,
|
||||||
else:
|
safe_component(client_name),
|
||||||
drawer_id, client_name, folder_parts, date, doc_name = result
|
*(safe_component(p) for p in folder_parts),
|
||||||
dest_dir = os.path.join(
|
)
|
||||||
output_dir,
|
dest_filename = doc_name
|
||||||
client_name,
|
parsed = True
|
||||||
*folder_parts,
|
|
||||||
)
|
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
|
success += 1
|
||||||
|
else:
|
||||||
os.makedirs(dest_dir, exist_ok=True)
|
failed += 1
|
||||||
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)
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user