From 5bf7962c043407b21bf283c0f96d9dd7915bf403 Mon Sep 17 00:00:00 2001 From: kqjy Date: Tue, 24 Feb 2026 20:41:39 +0800 Subject: [PATCH] Fix UI: versioning modals and object browser panel showing 'null' --- app/storage.py | 37 +++++++++++++----- static/js/bucket-detail-main.js | 69 ++++++++++++++++++++++++++++++--- templates/bucket_detail.html | 11 +++--- 3 files changed, 97 insertions(+), 20 deletions(-) diff --git a/app/storage.py b/app/storage.py index 683fd4b..47590f2 100644 --- a/app/storage.py +++ b/app/storage.py @@ -377,9 +377,18 @@ class ObjectStorage: raise StorageError("Bucket contains archived object versions") if has_multipart: raise StorageError("Bucket has active multipart uploads") + bucket_id = bucket_path.name self._remove_tree(bucket_path) - self._remove_tree(self._system_bucket_root(bucket_path.name)) - self._remove_tree(self._multipart_bucket_root(bucket_path.name)) + self._remove_tree(self._system_bucket_root(bucket_id)) + self._remove_tree(self._multipart_bucket_root(bucket_id)) + self._bucket_config_cache.pop(bucket_id, None) + with self._cache_lock: + self._object_cache.pop(bucket_id, None) + self._cache_version.pop(bucket_id, None) + self._sorted_key_cache.pop(bucket_id, None) + stale = [k for k in self._meta_read_cache if k[0] == bucket_id] + for k in stale: + del self._meta_read_cache[k] def list_objects( self, @@ -1832,30 +1841,40 @@ class ObjectStorage: def _read_bucket_config(self, bucket_name: str) -> dict[str, Any]: now = time.time() + config_path = self._bucket_config_path(bucket_name) cached = self._bucket_config_cache.get(bucket_name) if cached: - config, cached_time = cached + config, cached_time, cached_mtime = cached if now - cached_time < self._bucket_config_cache_ttl: - return config.copy() + try: + current_mtime = config_path.stat().st_mtime if config_path.exists() else 0.0 + except OSError: + current_mtime = 0.0 + if current_mtime == cached_mtime: + return config.copy() - config_path = self._bucket_config_path(bucket_name) if not config_path.exists(): - self._bucket_config_cache[bucket_name] = ({}, now) + self._bucket_config_cache[bucket_name] = ({}, now, 0.0) return {} try: data = json.loads(config_path.read_text(encoding="utf-8")) config = data if isinstance(data, dict) else {} - self._bucket_config_cache[bucket_name] = (config, now) + mtime = config_path.stat().st_mtime + self._bucket_config_cache[bucket_name] = (config, now, mtime) return config.copy() except (OSError, json.JSONDecodeError): - self._bucket_config_cache[bucket_name] = ({}, now) + self._bucket_config_cache[bucket_name] = ({}, now, 0.0) return {} def _write_bucket_config(self, bucket_name: str, payload: dict[str, Any]) -> None: config_path = self._bucket_config_path(bucket_name) config_path.parent.mkdir(parents=True, exist_ok=True) config_path.write_text(json.dumps(payload), encoding="utf-8") - self._bucket_config_cache[bucket_name] = (payload.copy(), time.time()) + try: + mtime = config_path.stat().st_mtime + except OSError: + mtime = 0.0 + self._bucket_config_cache[bucket_name] = (payload.copy(), time.time(), mtime) def _set_bucket_config_entry(self, bucket_name: str, key: str, value: Any | None) -> None: config = self._read_bucket_config(bucket_name) diff --git a/static/js/bucket-detail-main.js b/static/js/bucket-detail-main.js index 8e326fb..d194a3a 100644 --- a/static/js/bucket-detail-main.js +++ b/static/js/bucket-detail-main.js @@ -137,11 +137,11 @@ const versionPanel = document.getElementById('version-panel'); const versionList = document.getElementById('version-list'); const refreshVersionsButton = document.getElementById('refreshVersionsButton'); - const archivedCard = document.getElementById('archived-objects-card'); - const archivedBody = archivedCard?.querySelector('[data-archived-body]'); - const archivedCountBadge = archivedCard?.querySelector('[data-archived-count]'); - const archivedRefreshButton = archivedCard?.querySelector('[data-archived-refresh]'); - const archivedEndpoint = archivedCard?.dataset.archivedEndpoint; + let archivedCard = document.getElementById('archived-objects-card'); + let archivedBody = archivedCard?.querySelector('[data-archived-body]'); + let archivedCountBadge = archivedCard?.querySelector('[data-archived-count]'); + let archivedRefreshButton = archivedCard?.querySelector('[data-archived-refresh]'); + let archivedEndpoint = archivedCard?.dataset.archivedEndpoint; let versioningEnabled = objectsContainer?.dataset.versioning === 'true'; const versionsCache = new Map(); let activeRow = null; @@ -1737,6 +1737,15 @@ loadArchivedObjects(); } + const propertiesTab = document.getElementById('properties-tab'); + if (propertiesTab) { + propertiesTab.addEventListener('shown.bs.tab', () => { + if (archivedCard && archivedEndpoint) { + loadArchivedObjects(); + } + }); + } + async function restoreVersion(row, version) { if (!row || !version?.version_id) return; const template = row.dataset.restoreTemplate; @@ -4163,6 +4172,47 @@ var archivedCardEl = document.getElementById('archived-objects-card'); if (archivedCardEl) { archivedCardEl.style.display = enabled ? '' : 'none'; + } else if (enabled) { + var endpoint = window.BucketDetailConfig?.endpoints?.archivedObjects || ''; + if (endpoint) { + var html = '
' + + '
' + + '
' + + '' + + '' + + 'Archived Objects
' + + '
' + + '0 items' + + '
' + + '
' + + '

Objects that have been deleted while versioning is enabled. Their previous versions remain available until you restore or purge them.

' + + '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
' + + '' + + 'KeyLatest VersionVersionsActions
' + + '' + + '' + + 'No archived objects
'; + card.insertAdjacentHTML('afterend', html); + archivedCard = document.getElementById('archived-objects-card'); + archivedBody = archivedCard.querySelector('[data-archived-body]'); + archivedCountBadge = archivedCard.querySelector('[data-archived-count]'); + archivedRefreshButton = archivedCard.querySelector('[data-archived-refresh]'); + archivedEndpoint = endpoint; + archivedRefreshButton.addEventListener('click', function() { loadArchivedObjects(); }); + loadArchivedObjects(); + } } var dropZone = document.getElementById('objects-drop-zone'); @@ -4170,6 +4220,15 @@ dropZone.setAttribute('data-versioning', enabled ? 'true' : 'false'); } + var bulkPurgeWrap = document.getElementById('bulkDeletePurgeWrap'); + if (bulkPurgeWrap) { + bulkPurgeWrap.classList.toggle('d-none', !enabled); + } + var singleDeleteVerWrap = document.getElementById('deleteObjectVersioningWrap'); + if (singleDeleteVerWrap) { + singleDeleteVerWrap.classList.toggle('d-none', !enabled); + } + if (!enabled) { var newForm = document.getElementById('enableVersioningForm'); if (newForm) { diff --git a/templates/bucket_detail.html b/templates/bucket_detail.html index 14f3406..d4fc3ee 100644 --- a/templates/bucket_detail.html +++ b/templates/bucket_detail.html @@ -2272,13 +2272,11 @@
- {% if versioning_enabled %} -
+
Removes any archived versions stored in the archive.
- {% endif %}