diff --git a/backend/converter.py b/backend/converter.py index 8417320..583a818 100644 --- a/backend/converter.py +++ b/backend/converter.py @@ -110,18 +110,38 @@ def extract_if_needed(filename: str) -> Path: # If the archive had a single top-level directory matching the stem, use it expected_dir = STAGING_IN / stem if expected_dir in new_items and expected_dir.is_dir(): - return expected_dir + result = expected_dir + else: + # Collect scattered files into a subdirectory + out_dir = STAGING_IN / stem + out_dir.mkdir(parents=True, exist_ok=True) + for item in new_items: + shutil.move(str(item), str(out_dir / item.name)) + result = out_dir - # Otherwise, collect scattered files into a subdirectory - out_dir = STAGING_IN / stem - out_dir.mkdir(parents=True, exist_ok=True) - for item in new_items: - dest = out_dir / item.name - if item.is_dir(): - shutil.move(str(item), str(dest)) - else: - shutil.move(str(item), str(dest)) - return out_dir + # Flatten nested single-child directories (archive quirk) + result = _flatten_nested(result) + return result + + +def _flatten_nested(base: Path) -> Path: + """If a directory has exactly one child that is also a directory, + move the child's contents up and remove the child. Repeat until stable. + Returns the (possibly changed) base path.""" + changed = True + while changed: + changed = False + if not base.is_dir(): + break + entries = [e for e in base.iterdir() if e.name not in ('.', '..')] + if len(entries) == 1 and entries[0].is_dir(): + inner = entries[0] + # Move everything from inner into base + for item in list(inner.iterdir()): + shutil.move(str(item), str(base / item.name)) + inner.rmdir() + changed = True + return base def _detect_archive_ext(name: str) -> Optional[str]: