Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/src/content/docs/features/gallery.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ Additionally, each image has a context menu (right-click or Ctrl+click) with pow
Selecting **Delete Image** will remove the image entirely from your InvokeAI installation. This action cannot be undone.
:::

:::note
A deletion removes the image's gallery record first and its files immediately afterwards. If the record cannot be removed, the image is left completely untouched — it stays in the gallery with its files intact. If Invoke is interrupted, or the storage refuses the delete, after the record is gone, the leftover files are noted in a journal and cleaned up the next time Invoke starts. The same applies to **Clear Intermediates**.
:::

---

## Videos in the Gallery
Expand Down
34 changes: 22 additions & 12 deletions invokeai/app/api/routers/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,25 +232,35 @@ def delete_image(
_assert_image_owner(image_name, current_user)
assert_image_move_maintenance_inactive()

deleted_images: set[str] = set()
affected_boards: set[str] = set()

# Let service-level failures surface as errors rather than swallowing them and returning
# a success-shaped response. A previous version of this handler caught everything and
# returned an empty ``deleted_images`` list with HTTP 200; the frontend treated that as
# success and dropped the item from its cache even though the record was still live.
try:
image_dto = ApiDependencies.invoker.services.images.get_dto(image_name)
board_id = image_dto.board_id or "none"
except ImageRecordNotFoundException:
raise HTTPException(status_code=404, detail="Image not found")
except Exception:
# A record/URL/board lookup failure for an image that does exist is a server fault, not a
# missing image — reporting it as 404 would tell the frontend to drop a live item.
raise HTTPException(status_code=500, detail="Failed to delete image")

board_id = image_dto.board_id or "none"
try:
ApiDependencies.invoker.services.images.delete(image_name)
deleted_images.add(image_name)
affected_boards.add(board_id)
except ImageRecordNotFoundException:
# Another request deleted the image between the lookup above and the service call. The
# image is gone, which is what the client asked for — answer as the lookup would have.
raise HTTPException(status_code=404, detail="Image not found")
except Exception:
# TODO: Does this need any exception handling at all?
pass
raise HTTPException(status_code=500, detail="Failed to delete image")

return DeleteImagesResult(
deleted_images=list(deleted_images),
# Single-image route: the swallowed failure above already leaves deleted_images empty,
# which is how this route has always reported it.
deleted_images=[image_name],
# Every failure path above raises, so a returned result always describes a completed
# delete; nothing can land in ``failed_images``.
failed_images=[],
affected_boards=list(affected_boards),
affected_boards=[board_id],
)


Expand Down
24 changes: 22 additions & 2 deletions invokeai/app/services/image_files/image_files_base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from abc import ABC, abstractmethod
from collections.abc import Collection, Sequence
from pathlib import Path
from typing import Optional

Expand Down Expand Up @@ -67,8 +68,27 @@ def stage_delete(self, image_name: str, image_subfolder: str = "") -> object:
pass

@abstractmethod
def commit_delete(self, token: object) -> None:
"""Permanently removes files represented by a staged-delete token."""
def begin_delete(self, images: Sequence[tuple[str, str]]) -> object:
"""Durably records the intent to purge the (image_name, image_subfolder) pairs' files.

Call this before deleting the records, then ``commit_delete()`` after. If the process dies
in between, startup recovery uses the journal to purge the files of every listed image
whose record is gone, and leaves the files of every image whose record survives.
"""
pass

@abstractmethod
def commit_delete(self, token: object, image_names: Optional[Collection[str]] = None) -> None:
"""Permanently removes the files represented by a delete token.

``image_names`` narrows a pending-delete token to the records that were actually deleted;
it is ignored for a staged-delete token.
"""
pass

@abstractmethod
def abandon_delete(self, token: object) -> None:
"""Drops a pending-delete journal without purging anything, leaving the files in place."""
pass

@abstractmethod
Expand Down
Loading
Loading