mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-06-21 23:39:35 +08:00
Defer public asset file paths
Amp-Thread-ID: https://ampcode.com/threads/T-019ecf39-2e6f-747d-ae80-addba6b8e4f5 Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
parent
54d64d9762
commit
14266fe789
@ -39,7 +39,6 @@ from app.assets.services import (
|
||||
upload_from_temp_path,
|
||||
)
|
||||
from app.assets.services.cursor import InvalidCursorError
|
||||
from app.assets.services.path_utils import compute_api_file_path
|
||||
from app.assets.services.tagging import list_tag_histogram
|
||||
|
||||
ROUTES = web.RouteTableDef()
|
||||
@ -169,7 +168,6 @@ def _build_asset_response(result: schemas.AssetDetailResult | schemas.UploadResu
|
||||
asset_hash=asset_content_hash,
|
||||
size=int(result.asset.size_bytes) if result.asset else None,
|
||||
mime_type=result.asset.mime_type if result.asset else None,
|
||||
file_path=compute_api_file_path(result.ref.file_path),
|
||||
tags=result.tags,
|
||||
preview_url=preview_url,
|
||||
preview_id=result.ref.preview_id,
|
||||
|
||||
@ -14,7 +14,6 @@ class Asset(BaseModel):
|
||||
asset_hash: str | None = None
|
||||
size: int | None = None
|
||||
mime_type: str | None = None
|
||||
file_path: str | None = None
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
preview_url: str | None = None
|
||||
preview_id: str | None = None # references an asset_reference id, not an asset id
|
||||
|
||||
@ -114,25 +114,6 @@ def compute_relative_filename(file_path: str) -> str | None:
|
||||
return "/".join(parts) # input/output: keep all parts
|
||||
|
||||
|
||||
def compute_api_file_path(file_path: str | None) -> str | None:
|
||||
"""Return a stable API-visible path relative to a known asset root.
|
||||
|
||||
Examples:
|
||||
/.../input/foo.png -> "input/foo.png"
|
||||
/.../models/checkpoints/foo.safetensors -> "models/checkpoints/foo.safetensors"
|
||||
|
||||
Returns None for references without a filesystem path or paths outside
|
||||
known asset roots.
|
||||
"""
|
||||
if not file_path:
|
||||
return None
|
||||
try:
|
||||
root_category, rel_path = get_asset_category_and_relative_path(file_path)
|
||||
except ValueError:
|
||||
return None
|
||||
return "/".join([root_category, *Path(rel_path).parts])
|
||||
|
||||
|
||||
def get_asset_category_and_relative_path(
|
||||
file_path: str,
|
||||
) -> tuple[Literal["input", "output", "temp", "models"], str]:
|
||||
|
||||
16
openapi.yaml
16
openapi.yaml
@ -7,14 +7,6 @@ components:
|
||||
description: Timestamp when the asset was created
|
||||
format: date-time
|
||||
type: string
|
||||
display_name:
|
||||
description: Display name of the asset. Mirrors name for backwards compatibility.
|
||||
nullable: true
|
||||
type: string
|
||||
file_path:
|
||||
description: Relative path in global-namespace-root form (e.g. "models/checkpoints/flux.safetensors")
|
||||
nullable: true
|
||||
type: string
|
||||
hash:
|
||||
description: Blake3 hash of the asset content.
|
||||
pattern: ^blake3:[a-f0-9]{64}$
|
||||
@ -138,14 +130,6 @@ components:
|
||||
AssetUpdated:
|
||||
description: Response returned when an existing asset is successfully updated.
|
||||
properties:
|
||||
display_name:
|
||||
description: Display name of the asset. Mirrors name for backwards compatibility.
|
||||
nullable: true
|
||||
type: string
|
||||
file_path:
|
||||
description: Relative path in global-namespace-root form (e.g. "models/checkpoints/flux.safetensors")
|
||||
nullable: true
|
||||
type: string
|
||||
hash:
|
||||
description: Blake3 hash of the asset content.
|
||||
pattern: ^blake3:[a-f0-9]{64}$
|
||||
|
||||
@ -19,8 +19,8 @@ def test_seed_asset_removed_when_file_is_deleted(
|
||||
"""Asset without hash (seed) whose file disappears:
|
||||
after triggering sync_seed_assets, Asset + AssetInfo disappear.
|
||||
"""
|
||||
# Create a file directly under input/unit-tests/<case>. Path components are
|
||||
# exposed through file_path; backend tags only classify the root.
|
||||
# Create a file directly under input/unit-tests/<case>. Backend tags only
|
||||
# classify the root; nested path components are not exposed as tags.
|
||||
case_dir = comfy_tmp_base_dir / root / "unit-tests" / "syncseed"
|
||||
case_dir.mkdir(parents=True, exist_ok=True)
|
||||
name = f"seed_{uuid.uuid4().hex[:8]}.bin"
|
||||
@ -39,11 +39,7 @@ def test_seed_asset_removed_when_file_is_deleted(
|
||||
body1 = r1.json()
|
||||
assert r1.status_code == 200
|
||||
# there should be exactly one with that name
|
||||
expected_suffix = f"{root}/unit-tests/syncseed/{name}"
|
||||
matches = [
|
||||
a for a in body1.get("assets", [])
|
||||
if a.get("name") == name and a.get("file_path") == expected_suffix
|
||||
]
|
||||
matches = [a for a in body1.get("assets", []) if a.get("name") == name]
|
||||
assert matches
|
||||
# Seed assets have no hash; exclude_none drops both keys from the response
|
||||
assert "asset_hash" not in matches[0]
|
||||
@ -64,10 +60,7 @@ def test_seed_asset_removed_when_file_is_deleted(
|
||||
)
|
||||
body2 = r2.json()
|
||||
assert r2.status_code == 200
|
||||
matches2 = [
|
||||
a for a in body2.get("assets", [])
|
||||
if a.get("name") == name and a.get("file_path") == expected_suffix
|
||||
]
|
||||
matches2 = [a for a in body2.get("assets", []) if a.get("name") == name]
|
||||
assert not matches2, f"Seed asset {asset_info_id} should be gone after sync"
|
||||
|
||||
|
||||
@ -140,7 +133,7 @@ def test_hashed_asset_two_asset_infos_both_get_missing(
|
||||
second_id = b2["id"]
|
||||
|
||||
# Remove the single underlying file
|
||||
p = comfy_tmp_base_dir / created["file_path"]
|
||||
p = comfy_tmp_base_dir / "input" / get_asset_filename(created["asset_hash"], ".png")
|
||||
assert p.exists()
|
||||
p.unlink()
|
||||
|
||||
@ -258,7 +251,7 @@ def test_missing_tag_clears_on_fastpass_when_mtime_and_size_match(
|
||||
|
||||
a = asset_factory(name, [root, "unit-tests", scope], {}, data)
|
||||
aid = a["id"]
|
||||
p = comfy_tmp_base_dir / a["file_path"]
|
||||
p = comfy_tmp_base_dir / root / get_asset_filename(a["asset_hash"], ".bin")
|
||||
st0 = p.stat()
|
||||
orig_mtime_ns = getattr(st0, "st_mtime_ns", int(st0.st_mtime * 1_000_000_000))
|
||||
|
||||
|
||||
@ -295,11 +295,7 @@ def test_metadata_filename_is_set_for_seed_asset_without_hash(
|
||||
)
|
||||
body = r1.json()
|
||||
assert r1.status_code == 200, body
|
||||
expected_file_path = f"{root}/unit-tests/{scope}/a/b/{name}"
|
||||
matches = [
|
||||
a for a in body.get("assets", [])
|
||||
if a.get("name") == name and a.get("file_path") == expected_file_path
|
||||
]
|
||||
matches = [a for a in body.get("assets", []) if a.get("name") == name]
|
||||
assert matches, "Seed asset should be visible after sync"
|
||||
# Seed assets have no hash; exclude_none drops both keys from the response
|
||||
assert "asset_hash" not in matches[0]
|
||||
|
||||
@ -3,7 +3,7 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
from helpers import trigger_sync_seed_assets
|
||||
from helpers import get_asset_filename, trigger_sync_seed_assets
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -35,11 +35,6 @@ def find_asset(http: requests.Session, api_base: str):
|
||||
r = http.get(f"{api_base}/api/assets", params=params, timeout=120)
|
||||
assert r.status_code == 200
|
||||
assets = r.json().get("assets", [])
|
||||
expected_path_fragment = f"/unit-tests/{scope}/"
|
||||
assets = [
|
||||
a for a in assets
|
||||
if expected_path_fragment in f"/{a.get('file_path', '')}"
|
||||
]
|
||||
if name:
|
||||
return [a for a in assets if a.get("name") == name]
|
||||
return assets
|
||||
@ -96,7 +91,7 @@ def test_hashed_asset_not_pruned_when_file_missing(
|
||||
data = make_asset_bytes("test", 2048)
|
||||
a = asset_factory("test.bin", ["input", "unit-tests", scope], {}, data)
|
||||
|
||||
path = comfy_tmp_base_dir / a["file_path"]
|
||||
path = comfy_tmp_base_dir / "input" / get_asset_filename(a["asset_hash"], ".bin")
|
||||
path.unlink()
|
||||
|
||||
trigger_sync_seed_assets(http, api_base)
|
||||
@ -117,14 +112,14 @@ def test_prune_across_multiple_roots(
|
||||
create_seed_file("output", scope, "output.bin")
|
||||
|
||||
trigger_sync_seed_assets(http, api_base)
|
||||
assert len(find_asset(scope)) == 2
|
||||
assert find_asset(scope, input_fp.name)
|
||||
assert find_asset(scope, "output.bin")
|
||||
|
||||
input_fp.unlink()
|
||||
trigger_sync_seed_assets(http, api_base)
|
||||
|
||||
remaining = find_asset(scope)
|
||||
assert len(remaining) == 1
|
||||
assert remaining[0]["name"] == "output.bin"
|
||||
assert not find_asset(scope, input_fp.name)
|
||||
assert find_asset(scope, "output.bin")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dirname", ["100%_done", "my_folder_name", "has spaces"])
|
||||
|
||||
@ -381,7 +381,6 @@ def test_upload_subfolder_is_explicit_path_component(
|
||||
assert r.status_code == 201, body
|
||||
stored_name = get_asset_filename(body["asset_hash"], ".bin")
|
||||
assert (comfy_tmp_base_dir / "input" / "foo" / "bar" / stored_name).exists()
|
||||
assert body["file_path"] == f"input/foo/bar/{stored_name}"
|
||||
assert "foo" in body["tags"]
|
||||
|
||||
|
||||
@ -498,7 +497,6 @@ def test_multipart_upload_role_selects_write_location(
|
||||
stored_name = get_asset_filename(body["asset_hash"], extension)
|
||||
expected_disk_path = comfy_tmp_base_dir / expected_root / stored_name
|
||||
assert expected_disk_path.exists()
|
||||
assert body["file_path"] == f"{expected_root}/{stored_name}"
|
||||
|
||||
|
||||
def test_upload_empty_tags_rejected(http: requests.Session, api_base: str):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user