Skip to content

Deleting an image during an in-flight subfolder move orphans the relocated file on disk #69

Description

@lstein

Summary

Since image_subfolder_move_items.image_name gained ON DELETE CASCADE (PR #34), deleting an image while a subfolder move job is in flight now succeeds and silently leaves the relocated file on disk with nothing in the database referencing it.

Before the cascade this same delete failed with a FOREIGN KEY constraint failed / ImageRecordDeleteException. That was itself a bug — it also made every previously-moved image permanently undeletable — so the cascade is a net improvement. This issue tracks the narrower leak it leaves behind.

Why it happens

Image deletion is not gated on an active move job. _has_active_job_for_image is consulted only when planning a move (invokeai/app/services/image_moves/image_moves_default.py:438), never on the delete path.

During perform_filesystem_moves the file has already been relocated to new_subfolder and the item row is state='moved', but images.image_subfolder still holds the old value until commit_database_updates runs. ImagesService.delete() (invokeai/app/services/images/images_default.py:382-386) deletes the file using the stale record.image_subfolder, then deletes the record. stage_delete (invokeai/app/services/image_files/image_files_disk.py:237) tolerates a missing file at the old location, so nothing surfaces the discrepancy.

Reproduction

  1. Set image_subfolder_strategy to a non-flat value so move jobs run.
  2. Get a job into state moving with an item x.png in state moved — file physically at <root>/new/x.png, while images.image_subfolder is still old.
  3. Delete x.png through the API.

Observed: delete succeeds; the cascade removes the image_subfolder_move_items row; <root>/new/x.png remains on disk, unreferenced. _get_items(job_id) returns [].

Expected: either the delete is refused while a job is active for that image, or it removes the file at its current location.

Suggested fix

Gate ImagesService.delete() (and the bulk delete paths) on _has_active_job_for_image, or resolve the file's live location from the move item's new_path when an item is in state moved.

Context

Found during adversarial review of PR #34 and documented in the docstring of migration_2026_08_08_repair_image_subfolder_move_tables.py, which introduces the cascade. Deliberately not fixed there — the correct gate belongs in image_moves / images, not in a migration.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions