Skip to content

Commit 039ed38

Browse files
authored
fix(assets): remove unused delete_content param from deleteAsset (Comfy-Org#14241)
* fix(assets): remove unused delete_content param from deleteAsset The delete_content query param on DELETE /api/assets/{id} was introduced in Comfy-Org#12125 and had its default flipped to false in Comfy-Org#12621. In practice no client sends it: the frontend issues a bare DELETE /assets/{id}, so every real caller already gets the default soft-delete (the reference is hidden, content preserved). The only thing that set delete_content=true was this repo's own test teardown. Remove the param from the route and the OpenAPI spec so the contract matches what clients actually use (and lines up with the cloud surface). The route now always soft-deletes. The underlying delete_asset_reference helper keeps its delete_content_if_orphan option, so orphan reclamation remains available internally for a future GC path — it's just no longer exposed on the public endpoint. Tests that used delete_content=true for hard cleanup now soft-delete; test_delete_upon_reference_count asserts content preservation instead of orphan removal. * test/docs: address review on deleteAsset delete_content removal - Rename test_delete_upon_reference_count -> test_soft_delete_preserves_asset_identity_across_references; the old name implied last-ref cleanup, but it now verifies the opposite (soft delete preserves identity across references). - Strengthen the re-association assertion: also check asset_hash == src_hash so it proves content reuse rather than relying on the now-tautological created_new is False. - Document delete_asset_reference: the orphan-reclamation branch is intentionally internal-only; the public endpoint always soft-deletes. - Normalize the soft-delete comment phrasing. * test(assets): make seed content unique per test for isolation Removing the delete_content param means delete is always a soft delete, so content created by one test now survives into the next. The suite had been relying on hard-delete teardown for isolation, so shared fixed-content fixtures started colliding: seeded_asset (b"A"*4096) and make_asset_bytes (deterministic on name) produced the same hash every test, so the second seed deduped to the surviving asset and returned 200 instead of 201, cascading into ~14 failures/errors. Salt both fixtures with a per-test uuid so each test creates fresh content (created_new True, 201), while keeping content deterministic within a test (same name/size -> same bytes) and preserving exact byte length so size-based list/sort assertions are unaffected.
1 parent 84e0692 commit 039ed38

6 files changed

Lines changed: 46 additions & 24 deletions

File tree

app/assets/api/routes.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -533,18 +533,14 @@ async def update_asset_route(request: web.Request) -> web.Response:
533533
@_require_assets_feature_enabled
534534
async def delete_asset_route(request: web.Request) -> web.Response:
535535
reference_id = str(uuid.UUID(request.match_info["id"]))
536-
delete_content_param = request.query.get("delete_content")
537-
delete_content = (
538-
False
539-
if delete_content_param is None
540-
else delete_content_param.lower() not in {"0", "false", "no"}
541-
)
542536

543537
try:
538+
# Deleting an asset is a soft delete of the reference; the underlying
539+
# content is preserved (it may be shared with other references).
544540
deleted = delete_asset_reference(
545541
reference_id=reference_id,
546542
owner_id=USER_MANAGER.get_request_user_id(request),
547-
delete_content_if_orphan=delete_content,
543+
delete_content_if_orphan=False,
548544
)
549545
except Exception:
550546
logging.exception(

app/assets/services/asset_management.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,16 @@ def delete_asset_reference(
160160
owner_id: str,
161161
delete_content_if_orphan: bool = True,
162162
) -> bool:
163+
"""Delete an asset reference.
164+
165+
With ``delete_content_if_orphan=False`` (a soft delete), the reference is
166+
hidden and the underlying content is preserved. With ``True``, the content
167+
is also removed once it becomes orphaned.
168+
169+
Note: the public DELETE /api/assets/{id} endpoint always soft-deletes
170+
(passes ``False``); the orphan-reclamation path is intentionally
171+
internal-only, retained for a future GC/admin caller.
172+
"""
163173
with create_session() as session:
164174
if not delete_content_if_orphan:
165175
# Soft delete: mark the reference as deleted but keep everything

tests-unit/assets_test/conftest.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import sys
77
import tempfile
88
import time
9+
import uuid
910
from pathlib import Path
1011
from typing import Callable, Iterator, Optional
1112

@@ -188,9 +189,17 @@ def _post_multipart_asset(
188189

189190
@pytest.fixture
190191
def make_asset_bytes() -> Callable[[str, int], bytes]:
192+
# Salt content per test so it never collides with assets left over from
193+
# earlier tests. Delete is now always a soft delete (content is preserved),
194+
# so the suite can no longer rely on hard-deleting content for isolation.
195+
# Deterministic within a test: the same (name, size) yields the same bytes.
196+
salt = uuid.uuid4().bytes
197+
191198
def _make(name: str, size: int = 8192) -> bytes:
192199
seed = sum(ord(c) for c in name) % 251
193-
return bytes((i * 31 + seed) % 256 for i in range(size))
200+
body = bytearray((i * 31 + seed) % 256 for i in range(size))
201+
body[: len(salt)] = salt[:size]
202+
return bytes(body)
194203
return _make
195204

196205

@@ -212,7 +221,7 @@ def create(name: str, tags: list[str], meta: dict, data: bytes) -> dict:
212221

213222
for aid in created:
214223
with contextlib.suppress(Exception):
215-
http.delete(f"{api_base}/api/assets/{aid}?delete_content=true", timeout=30)
224+
http.delete(f"{api_base}/api/assets/{aid}", timeout=30)
216225

217226

218227
@pytest.fixture
@@ -227,7 +236,11 @@ def seeded_asset(request: pytest.FixtureRequest, http: requests.Session, api_bas
227236
if tags is None:
228237
tags = ["models", "checkpoints", "unit-tests", "alpha"]
229238
meta = {"purpose": "test", "epoch": 1, "flags": ["x", "y"], "nullable": None}
230-
files = {"file": (name, b"A" * 4096, "application/octet-stream")}
239+
# Unique content per test so the seed always creates a fresh asset (201).
240+
# Delete is now always a soft delete, so content from a prior test survives
241+
# and would otherwise dedup this upload into an existing asset (200).
242+
content = uuid.uuid4().bytes + b"A" * (4096 - 16)
243+
files = {"file": (name, content, "application/octet-stream")}
231244
form_data = {
232245
"tags": json.dumps(tags),
233246
"name": name,
@@ -260,4 +273,4 @@ def autoclean_unit_test_assets(http: requests.Session, api_base: str):
260273
break
261274
for aid in ids:
262275
with contextlib.suppress(Exception):
263-
http.delete(f"{api_base}/api/assets/{aid}?delete_content=true", timeout=30)
276+
http.delete(f"{api_base}/api/assets/{aid}", timeout=30)

tests-unit/assets_test/test_crud.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ def test_get_and_delete_asset(http: requests.Session, api_base: str, seeded_asse
4545
assert "user_metadata" in detail
4646
assert "filename" in detail["user_metadata"]
4747

48-
# DELETE (hard delete to also remove underlying asset and file)
49-
rd = http.delete(f"{api_base}/api/assets/{aid}?delete_content=true", timeout=120)
48+
# Soft delete — the reference is hidden, content is preserved
49+
rd = http.delete(f"{api_base}/api/assets/{aid}", timeout=120)
5050
assert rd.status_code == 204
5151

5252
# GET again -> 404
@@ -60,7 +60,7 @@ def test_soft_delete_hides_from_get(http: requests.Session, api_base: str, seede
6060
aid = seeded_asset["id"]
6161
asset_hash = seeded_asset["asset_hash"]
6262

63-
# Soft-delete (default, no delete_content param)
63+
# Soft delete — the reference is hidden, content is preserved
6464
rd = http.delete(f"{api_base}/api/assets/{aid}", timeout=120)
6565
assert rd.status_code == 204
6666

@@ -81,11 +81,10 @@ def test_soft_delete_hides_from_get(http: requests.Session, api_base: str, seede
8181
ids = [a["id"] for a in rl.json().get("assets", [])]
8282
assert aid not in ids
8383

84-
# Clean up: hard-delete the soft-deleted reference and orphaned asset
85-
http.delete(f"{api_base}/api/assets/{aid}?delete_content=true", timeout=120)
84+
# The reference is already soft-deleted; content is preserved.
8685

8786

88-
def test_delete_upon_reference_count(
87+
def test_soft_delete_preserves_asset_identity_across_references(
8988
http: requests.Session, api_base: str, seeded_asset: dict
9089
):
9190
# Create a second reference to the same asset via from-hash
@@ -119,16 +118,20 @@ def test_delete_upon_reference_count(
119118
rh2 = http.head(f"{api_base}/api/assets/hash/{src_hash}", timeout=120)
120119
assert rh2.status_code == 200 # asset identity preserved (soft delete)
121120

122-
# Re-associate via from-hash, then hard-delete -> orphan content removed
121+
# Re-associate via from-hash: it must reuse the same preserved content
122+
# (created_new False AND the same hash), proving the soft deletes did not
123+
# destroy the underlying asset. Then soft-delete again -> still preserved.
123124
r3 = http.post(f"{api_base}/api/assets/from-hash", json=payload, timeout=120)
124125
assert r3.status_code == 201, r3.json()
126+
assert r3.json()["created_new"] is False
127+
assert r3.json()["asset_hash"] == src_hash # reused the surviving content
125128
aid3 = r3.json()["id"]
126129

127-
rd3 = http.delete(f"{api_base}/api/assets/{aid3}?delete_content=true", timeout=120)
130+
rd3 = http.delete(f"{api_base}/api/assets/{aid3}", timeout=120)
128131
assert rd3.status_code == 204
129132

130133
rh3 = http.head(f"{api_base}/api/assets/hash/{src_hash}", timeout=120)
131-
assert rh3.status_code == 404 # orphan content removed
134+
assert rh3.status_code == 200 # content preserved (soft delete)
132135

133136

134137
def test_update_asset_fields(http: requests.Session, api_base: str, seeded_asset: dict):
@@ -249,7 +252,7 @@ def test_concurrent_delete_same_asset_info_single_204(
249252

250253
# Hit the same endpoint N times in parallel.
251254
n_tests = 4
252-
url = f"{api_base}/api/assets/{aid}?delete_content=false"
255+
url = f"{api_base}/api/assets/{aid}"
253256

254257
def _do_delete(delete_url):
255258
with requests.Session() as s:

tests-unit/assets_test/test_downloads.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ def test_download_missing_file_returns_404(
117117
assert body["error"]["code"] == "FILE_NOT_FOUND"
118118
finally:
119119
# We created asset without the "unit-tests" tag(see `autoclean_unit_test_assets`), we need to clear it manually.
120-
dr = http.delete(f"{api_base}/api/assets/{aid}?delete_content=true", timeout=120)
120+
dr = http.delete(f"{api_base}/api/assets/{aid}", timeout=120)
121121
dr.content
122122

123123

tests-unit/assets_test/test_tags_api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ def test_tags_empty_usage(http: requests.Session, api_base: str, asset_factory,
6969
used_names = [t["name"] for t in body2["tags"]]
7070
assert custom_tag in used_names
7171

72-
# Hard-delete the asset so the tag usage drops to zero
73-
rd = http.delete(f"{api_base}/api/assets/{_asset['id']}?delete_content=true", timeout=120)
72+
# Delete the asset reference so the tag usage drops to zero
73+
rd = http.delete(f"{api_base}/api/assets/{_asset['id']}", timeout=120)
7474
assert rd.status_code == 204
7575

7676
# Now the custom tag must not be returned when include_zero=false

0 commit comments

Comments
 (0)