Add new filetype previews; Remove metadata from bucket streaming
This commit is contained in:
30
app/ui.py
30
app/ui.py
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user