Add new filetype previews; Remove metadata from bucket streaming

This commit is contained in:
2026-01-17 15:40:58 +08:00
parent a52657e684
commit 9c3518de63
3 changed files with 88 additions and 47 deletions

View File

@@ -620,14 +620,10 @@ def list_bucket_objects(bucket_name: str):
tags_template = url_for("ui.object_tags", bucket_name=bucket_name, object_key="KEY_PLACEHOLDER")
copy_template = url_for("ui.copy_object", bucket_name=bucket_name, object_key="KEY_PLACEHOLDER")
move_template = url_for("ui.move_object", bucket_name=bucket_name, object_key="KEY_PLACEHOLDER")
metadata_template = url_for("ui.object_metadata", bucket_name=bucket_name, object_key="KEY_PLACEHOLDER")
objects_data = []
for obj in result.objects:
metadata = {}
try:
metadata = storage.get_object_metadata(bucket_name, obj.key)
except Exception:
pass
objects_data.append({
"key": obj.key,
"size": obj.size,
@@ -635,7 +631,6 @@ def list_bucket_objects(bucket_name: str):
"last_modified_display": _format_datetime_display(obj.last_modified),
"last_modified_iso": _format_datetime_iso(obj.last_modified),
"etag": obj.etag,
"metadata": metadata,
})
return jsonify({
@@ -654,6 +649,7 @@ def list_bucket_objects(bucket_name: str):
"tags": tags_template,
"copy": copy_template,
"move": move_template,
"metadata": metadata_template,
},
})
@@ -687,6 +683,7 @@ def stream_bucket_objects(bucket_name: str):
tags_template = url_for("ui.object_tags", bucket_name=bucket_name, object_key="KEY_PLACEHOLDER")
copy_template = url_for("ui.copy_object", bucket_name=bucket_name, object_key="KEY_PLACEHOLDER")
move_template = url_for("ui.move_object", bucket_name=bucket_name, object_key="KEY_PLACEHOLDER")
metadata_template = url_for("ui.object_metadata", bucket_name=bucket_name, object_key="KEY_PLACEHOLDER")
display_tz = current_app.config.get("DISPLAY_TIMEZONE", "UTC")
def generate():
@@ -703,6 +700,7 @@ def stream_bucket_objects(bucket_name: str):
"tags": tags_template,
"copy": copy_template,
"move": move_template,
"metadata": metadata_template,
},
}) + "\n"
yield meta_line
@@ -728,11 +726,6 @@ def stream_bucket_objects(bucket_name: str):
yield json.dumps({"type": "count", "total_count": total_count}) + "\n"
for obj in result.objects:
metadata = {}
try:
metadata = storage.get_object_metadata(bucket_name, obj.key)
except Exception:
pass
yield json.dumps({
"type": "object",
"key": obj.key,
@@ -741,7 +734,6 @@ def stream_bucket_objects(bucket_name: str):
"last_modified_display": _format_datetime_display(obj.last_modified, display_tz),
"last_modified_iso": _format_datetime_iso(obj.last_modified, display_tz),
"etag": obj.etag,
"metadata": metadata,
}) + "\n"
if not result.is_truncated:
@@ -1181,6 +1173,20 @@ def object_presign(bucket_name: str, object_key: str):
return jsonify(body), response.status_code
@ui_bp.get("/buckets/<bucket_name>/objects/<path:object_key>/metadata")
def object_metadata(bucket_name: str, object_key: str):
principal = _current_principal()
storage = _storage()
try:
_authorize_ui(principal, bucket_name, "read", object_key=object_key)
metadata = storage.get_object_metadata(bucket_name, object_key)
return jsonify({"metadata": metadata})
except IamError as exc:
return jsonify({"error": str(exc)}), 403
except StorageError as exc:
return jsonify({"error": str(exc)}), 404
@ui_bp.get("/buckets/<bucket_name>/objects/<path:object_key>/versions")
def object_versions(bucket_name: str, object_key: str):
principal = _current_principal()

View File

@@ -100,6 +100,7 @@
const previewPlaceholder = document.getElementById('preview-placeholder');
const previewImage = document.getElementById('preview-image');
const previewVideo = document.getElementById('preview-video');
const previewAudio = document.getElementById('preview-audio');
const previewIframe = document.getElementById('preview-iframe');
const downloadButton = document.getElementById('downloadButton');
const presignButton = document.getElementById('presignButton');
@@ -186,20 +187,20 @@
tr.dataset.objectRow = '';
tr.dataset.key = obj.key;
tr.dataset.size = obj.size;
tr.dataset.lastModified = obj.lastModified || obj.last_modified;
tr.dataset.lastModifiedDisplay = obj.lastModifiedDisplay || obj.last_modified_display || new Date(obj.lastModified || obj.last_modified).toLocaleString();
tr.dataset.lastModifiedIso = obj.lastModifiedIso || obj.last_modified_iso || obj.lastModified || obj.last_modified;
tr.dataset.etag = obj.etag;
tr.dataset.previewUrl = obj.previewUrl || obj.preview_url;
tr.dataset.downloadUrl = obj.downloadUrl || obj.download_url;
tr.dataset.presignEndpoint = obj.presignEndpoint || obj.presign_endpoint;
tr.dataset.deleteEndpoint = obj.deleteEndpoint || obj.delete_endpoint;
tr.dataset.metadata = typeof obj.metadata === 'string' ? obj.metadata : JSON.stringify(obj.metadata || {});
tr.dataset.versionsEndpoint = obj.versionsEndpoint || obj.versions_endpoint;
tr.dataset.restoreTemplate = obj.restoreTemplate || obj.restore_template;
tr.dataset.tagsUrl = obj.tagsUrl || obj.tags_url;
tr.dataset.copyUrl = obj.copyUrl || obj.copy_url;
tr.dataset.moveUrl = obj.moveUrl || obj.move_url;
tr.dataset.lastModified = obj.lastModified ?? obj.last_modified ?? '';
tr.dataset.lastModifiedDisplay = obj.lastModifiedDisplay ?? obj.last_modified_display ?? new Date(obj.lastModified || obj.last_modified).toLocaleString();
tr.dataset.lastModifiedIso = obj.lastModifiedIso ?? obj.last_modified_iso ?? obj.lastModified ?? obj.last_modified ?? '';
tr.dataset.etag = obj.etag ?? '';
tr.dataset.previewUrl = obj.previewUrl ?? obj.preview_url ?? '';
tr.dataset.downloadUrl = obj.downloadUrl ?? obj.download_url ?? '';
tr.dataset.presignEndpoint = obj.presignEndpoint ?? obj.presign_endpoint ?? '';
tr.dataset.deleteEndpoint = obj.deleteEndpoint ?? obj.delete_endpoint ?? '';
tr.dataset.metadataUrl = obj.metadataUrl ?? obj.metadata_url ?? '';
tr.dataset.versionsEndpoint = obj.versionsEndpoint ?? obj.versions_endpoint ?? '';
tr.dataset.restoreTemplate = obj.restoreTemplate ?? obj.restore_template ?? '';
tr.dataset.tagsUrl = obj.tagsUrl ?? obj.tags_url ?? '';
tr.dataset.copyUrl = obj.copyUrl ?? obj.copy_url ?? '';
tr.dataset.moveUrl = obj.moveUrl ?? obj.move_url ?? '';
const keyToShow = displayKey || obj.key;
const lastModDisplay = obj.lastModifiedDisplay || obj.last_modified_display || new Date(obj.lastModified || obj.last_modified).toLocaleDateString();
@@ -487,7 +488,7 @@
downloadUrl: urlTemplates ? buildUrlFromTemplate(urlTemplates.download, key) : '',
presignEndpoint: urlTemplates ? buildUrlFromTemplate(urlTemplates.presign, key) : '',
deleteEndpoint: urlTemplates ? buildUrlFromTemplate(urlTemplates.delete, key) : '',
metadata: '{}',
metadataUrl: urlTemplates ? buildUrlFromTemplate(urlTemplates.metadata, key) : '',
versionsEndpoint: urlTemplates ? buildUrlFromTemplate(urlTemplates.versions, key) : '',
restoreTemplate: urlTemplates ? urlTemplates.restore.replace('KEY_PLACEHOLDER', encodeURIComponent(key).replace(/%2F/g, '/')) : '',
tagsUrl: urlTemplates ? buildUrlFromTemplate(urlTemplates.tags, key) : '',
@@ -1411,15 +1412,30 @@
}
};
const INTERNAL_METADATA_KEYS = new Set([
'__etag__',
'__size__',
'__content_type__',
'__last_modified__',
'__storage_class__',
]);
const isInternalKey = (key) => INTERNAL_METADATA_KEYS.has(key.toLowerCase());
const renderMetadata = (metadata) => {
if (!previewMetadata || !previewMetadataList) return;
previewMetadataList.innerHTML = '';
if (!metadata || Object.keys(metadata).length === 0) {
if (!metadata) {
previewMetadata.classList.add('d-none');
return;
}
const userMetadata = Object.entries(metadata).filter(([key]) => !isInternalKey(key));
if (userMetadata.length === 0) {
previewMetadata.classList.add('d-none');
return;
}
previewMetadata.classList.remove('d-none');
Object.entries(metadata).forEach(([key, value]) => {
userMetadata.forEach(([key, value]) => {
const wrapper = document.createElement('div');
wrapper.className = 'metadata-entry';
const label = document.createElement('div');
@@ -1811,9 +1827,10 @@
}
const resetPreviewMedia = () => {
[previewImage, previewVideo, previewIframe].forEach((el) => {
[previewImage, previewVideo, previewAudio, previewIframe].forEach((el) => {
if (!el) return;
el.classList.add('d-none');
if (el.tagName === 'VIDEO') {
if (el.tagName === 'VIDEO' || el.tagName === 'AUDIO') {
el.pause();
el.removeAttribute('src');
}
@@ -1824,28 +1841,27 @@
previewPlaceholder.classList.remove('d-none');
};
function metadataFromRow(row) {
if (!row || !row.dataset.metadata) {
return null;
}
async function fetchMetadata(metadataUrl) {
if (!metadataUrl) return null;
try {
const parsed = JSON.parse(row.dataset.metadata);
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
return parsed;
const resp = await fetch(metadataUrl);
if (resp.ok) {
const data = await resp.json();
return data.metadata || {};
}
} catch (err) {
console.warn('Failed to parse metadata for row', err);
} catch (e) {
console.warn('Failed to load metadata', e);
}
return null;
}
function selectRow(row) {
async function selectRow(row) {
document.querySelectorAll('[data-object-row]').forEach((r) => r.classList.remove('table-active'));
row.classList.add('table-active');
previewEmpty.classList.add('d-none');
previewPanel.classList.remove('d-none');
activeRow = row;
renderMetadata(metadataFromRow(row));
renderMetadata(null);
previewKey.textContent = row.dataset.key;
previewSize.textContent = formatBytes(Number(row.dataset.size));
@@ -1868,18 +1884,36 @@
resetPreviewMedia();
const previewUrl = row.dataset.previewUrl;
const lower = row.dataset.key.toLowerCase();
if (lower.match(/\.(png|jpg|jpeg|gif|webp|svg)$/)) {
if (previewUrl && lower.match(/\.(png|jpg|jpeg|gif|webp|svg|ico|bmp)$/)) {
previewImage.src = previewUrl;
previewImage.classList.remove('d-none');
previewPlaceholder.classList.add('d-none');
} else if (lower.match(/\.(mp4|webm|ogg)$/)) {
} else if (previewUrl && lower.match(/\.(mp4|webm|ogv|mov|avi|mkv)$/)) {
previewVideo.src = previewUrl;
previewVideo.classList.remove('d-none');
previewPlaceholder.classList.add('d-none');
} else if (lower.match(/\.(txt|log|json|md|csv)$/)) {
} else if (previewUrl && lower.match(/\.(mp3|wav|flac|ogg|aac|m4a|wma)$/)) {
previewAudio.src = previewUrl;
previewAudio.classList.remove('d-none');
previewPlaceholder.classList.add('d-none');
} else if (previewUrl && lower.match(/\.(pdf)$/)) {
previewIframe.src = previewUrl;
previewIframe.style.minHeight = '500px';
previewIframe.classList.remove('d-none');
previewPlaceholder.classList.add('d-none');
} else if (previewUrl && lower.match(/\.(txt|log|json|md|csv|xml|html|htm|js|ts|py|java|c|cpp|h|css|scss|yaml|yml|toml|ini|cfg|conf|sh|bat)$/)) {
previewIframe.src = previewUrl;
previewIframe.style.minHeight = '200px';
previewIframe.classList.remove('d-none');
previewPlaceholder.classList.add('d-none');
}
const metadataUrl = row.dataset.metadataUrl;
if (metadataUrl) {
const metadata = await fetchMetadata(metadataUrl);
if (activeRow === row) {
renderMetadata(metadata);
}
}
}
@@ -3741,8 +3775,8 @@
});
const originalSelectRow = selectRow;
selectRow = (row) => {
originalSelectRow(row);
selectRow = async (row) => {
await originalSelectRow(row);
loadObjectTags(row);
};

View File

@@ -320,6 +320,7 @@
</div>
<img id="preview-image" class="img-fluid d-none w-100" alt="Object preview" style="display: block;" />
<video id="preview-video" class="w-100 d-none" controls style="display: block;"></video>
<audio id="preview-audio" class="w-100 d-none" controls style="display: block;"></audio>
<iframe id="preview-iframe" class="w-100 d-none" loading="lazy" style="min-height: 200px;"></iframe>
</div>
</div>