MyFSIO v0.3.1 Release #23

Merged
kqjy merged 5 commits from next into main 2026-02-26 09:42:37 +00:00
3 changed files with 97 additions and 20 deletions
Showing only changes of commit 5bf7962c04 - Show all commits

View File

@@ -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:
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)

View File

@@ -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 = '<div class="card shadow-sm mt-4" id="archived-objects-card" data-archived-endpoint="' + endpoint + '">' +
'<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">' +
'<div class="d-flex align-items-center">' +
'<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="text-warning me-2" viewBox="0 0 16 16">' +
'<path d="M0 2a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1v7.5a2.5 2.5 0 0 1-2.5 2.5h-9A2.5 2.5 0 0 1 1 12.5V5a1 1 0 0 1-1-1V2zm2 3v7.5A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5V5H2zm13-3H1v2h14V2zM5 7.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5z"/>' +
'</svg><span class="fw-semibold">Archived Objects</span></div>' +
'<div class="d-flex align-items-center gap-2">' +
'<span class="badge text-bg-secondary" data-archived-count>0 items</span>' +
'<button class="btn btn-outline-secondary btn-sm" type="button" data-archived-refresh>' +
'<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">' +
'<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>' +
'<path d="M8 4.466V.534a.25.25 0 0 0-.41-.192L5.23 2.308a.25.25 0 0 0 0 .384l2.36 1.966A.25.25 0 0 0 8 4.466z"/>' +
'</svg>Refresh</button></div></div>' +
'<div class="card-body">' +
'<p class="text-muted small mb-3">Objects that have been deleted while versioning is enabled. Their previous versions remain available until you restore or purge them.</p>' +
'<div class="table-responsive"><table class="table table-sm table-hover align-middle mb-0">' +
'<thead class="table-light"><tr>' +
'<th scope="col"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1 text-muted" viewBox="0 0 16 16">' +
'<path d="M4 0h5.293A1 1 0 0 1 10 .293L13.707 4a1 1 0 0 1 .293.707V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm5.5 1.5v2a1 1 0 0 0 1 1h2l-3-3z"/>' +
'</svg>Key</th>' +
'<th scope="col">Latest Version</th>' +
'<th scope="col" class="text-center">Versions</th>' +
'<th scope="col" class="text-end">Actions</th>' +
'</tr></thead>' +
'<tbody data-archived-body><tr><td colspan="4" class="text-center text-muted py-4">' +
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="mb-2 d-block mx-auto" viewBox="0 0 16 16">' +
'<path d="M0 2a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1v7.5a2.5 2.5 0 0 1-2.5 2.5h-9A2.5 2.5 0 0 1 1 12.5V5a1 1 0 0 1-1-1V2zm2 3v7.5A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5V5H2zm13-3H1v2h14V2zM5 7.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5z"/>' +
'</svg>No archived objects</td></tr></tbody>' +
'</table></div></div></div>';
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) {

View File

@@ -2272,13 +2272,11 @@
</div>
<ul class="list-group mb-3" id="bulkDeleteList" style="max-height: 200px; overflow-y: auto;"></ul>
<div class="text-muted small" id="bulkDeleteStatus"></div>
{% if versioning_enabled %}
<div class="form-check mt-3 p-3 bg-body-tertiary rounded-3">
<div class="form-check mt-3 p-3 bg-body-tertiary rounded-3 {% if not versioning_enabled %}d-none{% endif %}" id="bulkDeletePurgeWrap">
<input class="form-check-input" type="checkbox" id="bulkDeletePurge" />
<label class="form-check-label" for="bulkDeletePurge">Also delete archived versions</label>
<div class="form-text">Removes any archived versions stored in the archive.</div>
</div>
{% endif %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
@@ -2316,7 +2314,7 @@
<div class="p-3 bg-body-tertiary rounded-3 mb-3">
<code id="deleteObjectKey" class="d-block text-break"></code>
</div>
{% if versioning_enabled %}
<div id="deleteObjectVersioningWrap" class="{% if not versioning_enabled %}d-none{% endif %}">
<div class="alert alert-warning d-flex align-items-start small mb-3" role="alert">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="flex-shrink-0 me-2 mt-0" viewBox="0 0 16 16">
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/>
@@ -2328,7 +2326,7 @@
<label class="form-check-label" for="deletePurgeVersions">Also delete all archived versions</label>
<div class="form-text mb-0">Removes the live object and every stored version.</div>
</div>
{% endif %}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
@@ -2771,7 +2769,8 @@
window.BucketDetailConfig = {
endpoints: {
versioning: "{{ url_for('ui.update_bucket_versioning', bucket_name=bucket_name) }}",
bucketsOverview: "{{ url_for('ui.buckets_overview') }}"
bucketsOverview: "{{ url_for('ui.buckets_overview') }}",
archivedObjects: "{{ url_for('ui.archived_objects', bucket_name=bucket_name) }}"
}
};