diff --git a/custom_components/houseplan/asset_integrity.py b/custom_components/houseplan/asset_integrity.py index c641b582..77248ade 100644 --- a/custom_components/houseplan/asset_integrity.py +++ b/custom_components/houseplan/asset_integrity.py @@ -3,6 +3,7 @@ from __future__ import annotations import hashlib import logging +import stat import threading from collections import OrderedDict from dataclasses import dataclass @@ -15,6 +16,7 @@ _LOGGER = logging.getLogger(__name__) ASSET_INTEGRITY_CACHE_ENTRIES = 256 ASSET_HASH_CHUNK_BYTES = 64 * 1024 +ASSET_INTEGRITY_FOLLOWER_TIMEOUT_SECONDS = 30.0 _HASS_DATA_KEY = "asset_integrity_verifier" @@ -40,11 +42,13 @@ class _Flight: def _signature(path: Path) -> FileSignature: - stat = path.stat() + current = path.stat() + if not stat.S_ISREG(current.st_mode): + raise OSError("asset is not a regular file") return FileSignature( - size=stat.st_size, - mtime_ns=stat.st_mtime_ns, - ctime_ns=stat.st_ctime_ns, + size=current.st_size, + mtime_ns=current.st_mtime_ns, + ctime_ns=current.st_ctime_ns, ) @@ -105,7 +109,8 @@ class AssetIntegrityVerifier: assert flight is not None if not owner: - flight.event.wait() + if not flight.event.wait(ASSET_INTEGRITY_FOLLOWER_TIMEOUT_SECONDS): + return False return flight.digest == expected_digest digest: str | None = None diff --git a/custom_components/houseplan/decor_assets.py b/custom_components/houseplan/decor_assets.py index 1ea90a7f..d07572d0 100644 --- a/custom_components/houseplan/decor_assets.py +++ b/custom_components/houseplan/decor_assets.py @@ -373,25 +373,37 @@ def asset_meta_path(root: Path, asset_id: str) -> Path: return root / f"{asset_id}.json" +def _physical_asset_inventory(root: Path) -> list[tuple[Path, int]]: + """Observe regular promoted blobs once, tolerating concurrent removals.""" + try: + if not root.is_dir(): + return [] + entries = list(root.iterdir()) + except OSError: + return [] + + inventory: list[tuple[Path, int]] = [] + for path in entries: + if path.suffix not in ASSET_EXTENSIONS or not ASSET_ID_RE.fullmatch(path.stem): + continue + try: + current = path.stat(follow_symlinks=False) + except OSError: + continue + if stat.S_ISREG(current.st_mode): + inventory.append((path, current.st_size)) + return sorted(inventory, key=lambda item: item[0].name) + + def physical_asset_blobs(root: Path) -> list[Path]: """Return exact promoted blob files, independently from their sidecars.""" - if not root.is_dir(): - return [] - blobs: list[Path] = [] - for path in root.iterdir(): - if ( - path.suffix in ASSET_EXTENSIONS - and ASSET_ID_RE.fullmatch(path.stem) - and stat.S_ISREG(path.stat(follow_symlinks=False).st_mode) - ): - blobs.append(path) - return sorted(blobs, key=lambda path: path.name) + return [path for path, _size in _physical_asset_inventory(root)] def physical_asset_usage(root: Path) -> tuple[int, int]: """Return promoted blob count and actual bytes used for quota checks.""" - blobs = physical_asset_blobs(root) - return len(blobs), sum(path.stat().st_size for path in blobs) + inventory = _physical_asset_inventory(root) + return len(inventory), sum(size for _path, size in inventory) def _read_catalog_row(root: Path, path: Path) -> dict[str, Any] | None: diff --git a/custom_components/houseplan/frontend/houseplan-assets.json b/custom_components/houseplan/frontend/houseplan-assets.json index 6084dc30..cb2fe05a 100644 --- a/custom_components/houseplan/frontend/houseplan-assets.json +++ b/custom_components/houseplan/frontend/houseplan-assets.json @@ -1,125 +1,125 @@ { "schema": 1, - "fingerprint": "a1c6b7fa80c55b69f45f0b0975bcde7eb67d53a02f75f5b7c61d66f220e78ed9", + "fingerprint": "060ec710f4ade532364b8887066629a45636ac2a8e16af549ec8f323c449b8a9", "entry": "houseplan-card.js", "initialViewFiles": [ - "houseplan-assets/houseplan-card-D9kogu9G.js", + "houseplan-assets/houseplan-card-Cq0kQMae.js", "houseplan-card.js" ], - "initialViewGzipBytes": 291175, + "initialViewGzipBytes": 291190, "lazyFiles": [ - "houseplan-assets/backdrop-pick-BEQcDs80.js", - "houseplan-assets/de-B-zT3Bet.js", - "houseplan-assets/editor-DJZS-SMT.js", - "houseplan-assets/fr-DhlZhAWs.js", - "houseplan-assets/houseplan-editor-runtime-S0bYVW5I.js", - "houseplan-assets/houseplan-onboarding-runtime-DJriHHug.js" + "houseplan-assets/backdrop-pick--dMAHFcA.js", + "houseplan-assets/de-1N1nfNI0.js", + "houseplan-assets/editor-yDIoOZQr.js", + "houseplan-assets/fr-CY0KFbzM.js", + "houseplan-assets/houseplan-editor-runtime-Dv0DngiN.js", + "houseplan-assets/houseplan-onboarding-runtime-goBKaVug.js" ], - "lazyGzipBytes": 215559, + "lazyGzipBytes": 215576, "lazyEditorFiles": [ - "houseplan-assets/backdrop-pick-BEQcDs80.js", - "houseplan-assets/editor-DJZS-SMT.js", - "houseplan-assets/houseplan-editor-runtime-S0bYVW5I.js" + "houseplan-assets/backdrop-pick--dMAHFcA.js", + "houseplan-assets/editor-yDIoOZQr.js", + "houseplan-assets/houseplan-editor-runtime-Dv0DngiN.js" ], - "lazyEditorGzipBytes": 159876, + "lazyEditorGzipBytes": 159881, "lazyOnboardingFiles": [ - "houseplan-assets/backdrop-pick-BEQcDs80.js", - "houseplan-assets/houseplan-onboarding-runtime-DJriHHug.js" + "houseplan-assets/backdrop-pick--dMAHFcA.js", + "houseplan-assets/houseplan-onboarding-runtime-goBKaVug.js" ], - "lazyOnboardingGzipBytes": 14133, + "lazyOnboardingGzipBytes": 14134, "lazyLocaleFiles": [ - "houseplan-assets/de-B-zT3Bet.js", - "houseplan-assets/fr-DhlZhAWs.js" + "houseplan-assets/de-1N1nfNI0.js", + "houseplan-assets/fr-CY0KFbzM.js" ], - "lazyLocaleGzipBytes": 48727, + "lazyLocaleGzipBytes": 48739, "files": [ { - "path": "houseplan-assets/backdrop-pick-BEQcDs80.js", - "sha256": "b556cef2823ea5e4f7c142ee5f4cee8dbbed71311dd101291faf30a0d38d6805", + "path": "houseplan-assets/backdrop-pick--dMAHFcA.js", + "sha256": "982ddc9c1b3ecbfb0dfb63703a0bf35f4a1c294b4cb612e1734f848e328db03d", "rawBytes": 20996, - "gzipBytes": 7177, + "gzipBytes": 7178, "isEntry": false, "imports": [ - "houseplan-assets/houseplan-card-D9kogu9G.js" + "houseplan-assets/houseplan-card-Cq0kQMae.js" ], "dynamicImports": [] }, { - "path": "houseplan-assets/de-B-zT3Bet.js", - "sha256": "2ee98742a517ac754434e5627f958a20def03cd1c3907dd7ec3e394104c850a1", + "path": "houseplan-assets/de-1N1nfNI0.js", + "sha256": "5ebb658cfb7e05c3f1e074127e8fbead0580121b3f6dc8c390075ef5550f85c4", "rawBytes": 81632, - "gzipBytes": 24635, + "gzipBytes": 24641, "isEntry": false, "imports": [], "dynamicImports": [] }, { - "path": "houseplan-assets/editor-DJZS-SMT.js", - "sha256": "3cda88a32916d4a3256cb0f76570d72f58308ad9ee6a28c9496d5c9e2c613aa4", + "path": "houseplan-assets/editor-yDIoOZQr.js", + "sha256": "f70e69874730180ecdf67a7ee9003f5c3564432e2c370d235025bcda17696054", "rawBytes": 3826, - "gzipBytes": 1580, + "gzipBytes": 1582, "isEntry": false, "imports": [ - "houseplan-assets/houseplan-card-D9kogu9G.js" + "houseplan-assets/houseplan-card-Cq0kQMae.js" ], "dynamicImports": [] }, { - "path": "houseplan-assets/fr-DhlZhAWs.js", - "sha256": "f37aecbd6a5aaa1fbc5c3be0854464b949cbdcefd453906b268c46d76760772d", + "path": "houseplan-assets/fr-CY0KFbzM.js", + "sha256": "f356e5b756d5d1746567c1e687c709799af7f842d7de8107d5efdb9107716321", "rawBytes": 83754, - "gzipBytes": 24092, + "gzipBytes": 24098, "isEntry": false, "imports": [], "dynamicImports": [] }, { - "path": "houseplan-assets/houseplan-card-D9kogu9G.js", - "sha256": "65622583b77e3ff11a34ff2c2ceb65748cb1f607412794141375841fcea818bb", - "rawBytes": 1033646, - "gzipBytes": 290380, + "path": "houseplan-assets/houseplan-card-Cq0kQMae.js", + "sha256": "d3064f61d80066e24759cac73e23afc0f4c624d813b29c0f9ff30d12d7fea4a4", + "rawBytes": 1033685, + "gzipBytes": 290393, "isEntry": false, "imports": [], "dynamicImports": [ - "houseplan-assets/de-B-zT3Bet.js", - "houseplan-assets/editor-DJZS-SMT.js", - "houseplan-assets/fr-DhlZhAWs.js", - "houseplan-assets/houseplan-editor-runtime-S0bYVW5I.js", - "houseplan-assets/houseplan-onboarding-runtime-DJriHHug.js" + "houseplan-assets/de-1N1nfNI0.js", + "houseplan-assets/editor-yDIoOZQr.js", + "houseplan-assets/fr-CY0KFbzM.js", + "houseplan-assets/houseplan-editor-runtime-Dv0DngiN.js", + "houseplan-assets/houseplan-onboarding-runtime-goBKaVug.js" ] }, { - "path": "houseplan-assets/houseplan-editor-runtime-S0bYVW5I.js", - "sha256": "c1935afb94bfe01cc7ee1302492c6c9a76faac8bd60ddc1db98eded52c7d9d65", + "path": "houseplan-assets/houseplan-editor-runtime-Dv0DngiN.js", + "sha256": "098fd4c1331b45578ac8d07a0eb351b1202a5cc3d16cefba88ed17c5e1bb6a4c", "rawBytes": 576162, - "gzipBytes": 151119, + "gzipBytes": 151121, "isEntry": false, "imports": [ - "houseplan-assets/backdrop-pick-BEQcDs80.js", - "houseplan-assets/houseplan-card-D9kogu9G.js" + "houseplan-assets/backdrop-pick--dMAHFcA.js", + "houseplan-assets/houseplan-card-Cq0kQMae.js" ], "dynamicImports": [] }, { - "path": "houseplan-assets/houseplan-onboarding-runtime-DJriHHug.js", - "sha256": "20b122261b8ce7484806fbaaf17aafb5f59c1864648b1a31fa226dc861d7cbd2", + "path": "houseplan-assets/houseplan-onboarding-runtime-goBKaVug.js", + "sha256": "1b3cbd71e89904dc93378c95df0e0bc230787f9a4d1b2bfbd5d09874f93fce8b", "rawBytes": 28088, "gzipBytes": 6956, "isEntry": false, "imports": [ - "houseplan-assets/backdrop-pick-BEQcDs80.js", - "houseplan-assets/houseplan-card-D9kogu9G.js" + "houseplan-assets/backdrop-pick--dMAHFcA.js", + "houseplan-assets/houseplan-card-Cq0kQMae.js" ], "dynamicImports": [] }, { "path": "houseplan-card.js", - "sha256": "ac14b26a8d45a1747833be382ab3b0b816332156a59ed7125fa3eba598cd940d", + "sha256": "2d498cb03fe036c36702907335bcf53ee43ba2e58a5e445ee7de73b745365757", "rawBytes": 1183, - "gzipBytes": 795, + "gzipBytes": 797, "isEntry": true, "imports": [ - "houseplan-assets/houseplan-card-D9kogu9G.js" + "houseplan-assets/houseplan-card-Cq0kQMae.js" ], "dynamicImports": [] } diff --git a/custom_components/houseplan/frontend/houseplan-assets/backdrop-pick-BEQcDs80.js b/custom_components/houseplan/frontend/houseplan-assets/backdrop-pick--dMAHFcA.js similarity index 99% rename from custom_components/houseplan/frontend/houseplan-assets/backdrop-pick-BEQcDs80.js rename to custom_components/houseplan/frontend/houseplan-assets/backdrop-pick--dMAHFcA.js index c26f1bb5..b7d820a7 100644 --- a/custom_components/houseplan/frontend/houseplan-assets/backdrop-pick-BEQcDs80.js +++ b/custom_components/houseplan/frontend/houseplan-assets/backdrop-pick--dMAHFcA.js @@ -1,4 +1,4 @@ -globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="a1c6b7fa80c55b69f45f0b0975bcde7eb67d53a02f75f5b7c61d66f220e78ed9";import{dg as e,c as t,A as i,a as o,b as n,dh as s}from"./houseplan-card-D9kogu9G.js";function r(e){const t=e.visualViewport;return t&&Number.isFinite(t.width)&&Number.isFinite(t.height)?{left:Number(t.offsetLeft)||0,top:Number(t.offsetTop)||0,width:Math.max(0,Number(t.width)||0),height:Math.max(0,Number(t.height)||0)}:{left:0,top:0,width:e.innerWidth,height:e.innerHeight}}function a(e,t,i,o=7,n=8){const s=i.left+n,r=i.top+n,a=Math.max(s,i.left+i.width-n),l=Math.max(r,i.top+i.height-n),h=Math.max(0,a-s),c=Math.max(0,l-r),d=Math.min(Math.max(0,t.width),h),u=Math.min(Math.max(0,t.height),c);let p=e.left;p+d>a&&(p=e.right-d),p=Math.min(Math.max(s,p),Math.max(s,a-d));const _=e.bottom+o,g=e.top-o-u;let m="bottom",f=_;return _+u>l&&g>=r?(m="top",f=Math.min(Math.max(r,g),Math.max(r,l-u))):f=Math.min(Math.max(r,_),Math.max(r,l-u)),{left:Math.round(p),top:Math.round(f),side:m,maxWidth:Math.round(h),maxHeight:Math.round(c)}}class l{constructor(e,t,i){this.owner=e,this.name=t,this.keydown=i,this.host=null,this.root=null}window(){return this.owner.ownerDocument.defaultView}dialog(){return this.owner.closest("hp-dialog")}usesPopover(e=!1){const t=this.window();return!e&&!!t&&function(e){const t=e.HTMLElement?.prototype;return"function"==typeof t?.showPopover&&"function"==typeof t?.hidePopover}(t)}get hasFallback(){return!!this.host}containsPath(e){return e.includes(this.owner)||!!this.host&&e.includes(this.host)}ownsActiveElement(){let e=this.owner.ownerDocument.activeElement;for(;e?.shadowRoot?.activeElement;)e=e.shadowRoot.activeElement;return!(!e||e!==this.owner&&!this.owner.shadowRoot?.contains(e)&&!this.root?.contains(e))}renderFallback(i,o){const n=this.ensureFallback();return n?(e(t`${i}`,n),n):null}surface(e,t){return t?this.owner.shadowRoot?.querySelector(e)||null:this.root?.querySelector(e)||null}destroy(){this.root&&e(i,this.root),this.host?.remove(),this.host=null,this.root=null}ensureFallback(){if(this.root?.isConnected)return this.root;const e=this.dialog()?.overlayPortal()||this.owner.ownerDocument.body;if(!e)return null;const t=this.owner.ownerDocument.createElement("div");return t.dataset.hpOverlay=this.name,t.style.display="contents",this.keydown&&t.addEventListener("keydown",this.keydown,!0),e.append(t),this.host=t,this.root=t.attachShadow({mode:"open"}),this.root}}let h=0;class c extends n{constructor(){super(...arguments),this.text="",this.ariaLabel="",this.pointerHover=!1,this._open=!1,this._openTimer=0,this._closeTimer=0,this._positionRaf=0,this._forceFallback=!1,this._hoverOwned=!1,this._overlayDispose=null,this._scrollDialog=null,this._descriptionId="hp-help-description-"+ ++h,this._surfacePointerEnter=()=>{const e=this._window();this._closeTimer&&e?.clearTimeout(this._closeTimer),this._closeTimer=0},this._surfacePointerLeave=()=>{this._scheduleClose()},this._outsidePointerDown=e=>{if(!this._open)return;const t=e.composedPath();this._floating.containsPath(t)||this._closeHelp(!1,"outside")},this._dialogScroll=e=>{const t=this._floating.containsPath(e.composedPath()),i=e.target,o=this._dialog(),n=i instanceof Node&&(i===o||i instanceof Element&&i.contains(this));var s;this._open&&(s=n,!t&&s)&&this._closeHelp(!1,"scroll")},this._keyDown=e=>{if(!this._open||e.defaultPrevented)return;if("Escape"===e.key)return e.preventDefault(),e.stopImmediatePropagation(),void this._closeHelp(this._floating.ownsActiveElement(),"escape");const t=this.renderRoot.querySelector(".trigger");if(!t||!e.composedPath().includes(t))return;const i=this._surface();if(!i||i.scrollHeight<=i.clientHeight)return;const o=Math.max(40,.8*i.clientHeight),n={ArrowDown:40,ArrowUp:-40,PageDown:o,PageUp:-o};if("Home"===e.key)i.scrollTop=0;else if("End"===e.key)i.scrollTop=i.scrollHeight;else{if(!(e.key in n))return;i.scrollBy({top:n[e.key],behavior:"auto"})}e.preventDefault(),e.stopImmediatePropagation()},this._floating=new l(this,"help",this._keyDown),this._queuePosition=()=>{if(!this._open||this._positionRaf)return;const e=this._window();e&&(this._positionRaf=e.requestAnimationFrame(()=>{this._positionRaf=0,this._position()||this._closeHelp()}))}}connectedCallback(){super.connectedCallback(),this.addEventListener("keydown",this._keyDown,!0)}willUpdate(){this.toggleAttribute("data-has-content",this._hasContent())}disconnectedCallback(){const e=this.ownerDocument.defaultView;this._unsubscribeOpenListeners(),this.removeEventListener("keydown",this._keyDown,!0),this._clearTimers(),this._positionRaf&&e?.cancelAnimationFrame(this._positionRaf),this._positionRaf=0,this._closeHelp(!1,"disconnect"),super.disconnectedCallback()}updated(e){if(e.has("pointerHover")&&!this.pointerHover){const e=this._window();this._openTimer&&e?.clearTimeout(this._openTimer),this._openTimer=0,this._hoverOwned&&this._closeHelp(!1,"outside")}this._hasContent()||!this._open?this._open&&e.has("text")&&(this._usesPopover()||this._renderFallback(),this._queuePosition()):this._closeHelp()}_window(){return this._floating.window()}_dialog(){return this._floating.dialog()}_usesPopover(){return this._floating.usesPopover(this._forceFallback)}_hasContent(){return e=this.text,t=this.ariaLabel,"string"==typeof e&&e.trim().length>0&&"string"==typeof t&&t.trim().length>0;var e,t}_clearTimers(){const e=this._window();this._openTimer&&e?.clearTimeout(this._openTimer),this._closeTimer&&e?.clearTimeout(this._closeTimer),this._openTimer=0,this._closeTimer=0}_scheduleOpen(){const e=this._window();this.pointerHover&&e&&!this._open&&!this._openTimer&&(this._closeTimer&&e.clearTimeout(this._closeTimer),this._closeTimer=0,this._openTimer=e.setTimeout(()=>{this._openTimer=0,this.isConnected&&(this._hoverOwned=!0,this._openHelp())},300))}_scheduleClose(){const e=this._window(),t=this.renderRoot.querySelector(".trigger");e&&!t?.matches(":focus-visible")&&(this._openTimer&&e.clearTimeout(this._openTimer),this._closeTimer&&e.clearTimeout(this._closeTimer),this._openTimer=0,this._closeTimer=e.setTimeout(()=>{this._closeTimer=0,this.isConnected&&this._closeHelp()},150))}_triggerPointerEnter(e){this.pointerHover&&"mouse"===e.pointerType&&this._scheduleOpen()}_triggerPointerLeave(e){"mouse"===e.pointerType&&this._scheduleClose()}_triggerFocus(){queueMicrotask(()=>{const e=this.renderRoot.querySelector(".trigger");e?.matches(":focus-visible")&&(this._hoverOwned=!1,this._openHelp())})}_triggerBlur(){this._scheduleClose()}_triggerClick(){this._open?this._closeHelp():(this._hoverOwned=!1,this._openHelp())}_subscribeOpenListeners(){this.ownerDocument.addEventListener("pointerdown",this._outsidePointerDown,!0),this.ownerDocument.addEventListener("keydown",this._keyDown,!0);const e=this._window();e?.addEventListener("resize",this._queuePosition),e?.addEventListener("orientationchange",this._queuePosition),e?.visualViewport?.addEventListener("resize",this._queuePosition),e?.visualViewport?.addEventListener("scroll",this._queuePosition),this._scrollDialog=this._dialog(),this._scrollDialog?.addEventListener("scroll",this._dialogScroll,!0)}_unsubscribeOpenListeners(){this.ownerDocument.removeEventListener("pointerdown",this._outsidePointerDown,!0),this.ownerDocument.removeEventListener("keydown",this._keyDown,!0);const e=this._window();e?.removeEventListener("resize",this._queuePosition),e?.removeEventListener("orientationchange",this._queuePosition),e?.visualViewport?.removeEventListener("resize",this._queuePosition),e?.visualViewport?.removeEventListener("scroll",this._queuePosition),this._scrollDialog?.removeEventListener("scroll",this._dialogScroll,!0),this._scrollDialog=null}async _openHelp(){!this._open&&this._hasContent()&&(this._clearTimers(),this._forceFallback=!1,this._open=!0,this._subscribeOpenListeners(),await this.updateComplete,this._open&&(this._usesPopover()||this._renderFallback(),this._position()?this._overlayDispose=this._dialog()?.registerOverlay({owner:this,group:"transient",close:e=>this._closeHelp("escape"===e&&this._floating.ownsActiveElement(),e)})||null:this._closeHelp()))}_closeHelp(e=!1,t="exclusive"){if(!this._open&&!this._floating.hasFallback)return;this._clearTimers();const i=this._overlayDispose;this._overlayDispose=null,i?.();const o=this.renderRoot.querySelector(".tooltip");if(o?.hidePopover)try{o.matches(":popover-open")&&o.hidePopover()}catch{}this._open=!1,this._hoverOwned=!1,this._unsubscribeOpenListeners(),this._floating.destroy(),e&&this.updateComplete.then(()=>this.renderRoot.querySelector(".trigger")?.focus())}_tooltipTemplate(e){return t`