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")
|
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")
|
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")
|
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 = []
|
objects_data = []
|
||||||
for obj in result.objects:
|
for obj in result.objects:
|
||||||
metadata = {}
|
|
||||||
try:
|
|
||||||
metadata = storage.get_object_metadata(bucket_name, obj.key)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
objects_data.append({
|
objects_data.append({
|
||||||
"key": obj.key,
|
"key": obj.key,
|
||||||
"size": obj.size,
|
"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_display": _format_datetime_display(obj.last_modified),
|
||||||
"last_modified_iso": _format_datetime_iso(obj.last_modified),
|
"last_modified_iso": _format_datetime_iso(obj.last_modified),
|
||||||
"etag": obj.etag,
|
"etag": obj.etag,
|
||||||
"metadata": metadata,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@@ -654,6 +649,7 @@ def list_bucket_objects(bucket_name: str):
|
|||||||
"tags": tags_template,
|
"tags": tags_template,
|
||||||
"copy": copy_template,
|
"copy": copy_template,
|
||||||
"move": move_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")
|
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")
|
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")
|
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")
|
display_tz = current_app.config.get("DISPLAY_TIMEZONE", "UTC")
|
||||||
|
|
||||||
def generate():
|
def generate():
|
||||||
@@ -703,6 +700,7 @@ def stream_bucket_objects(bucket_name: str):
|
|||||||
"tags": tags_template,
|
"tags": tags_template,
|
||||||
"copy": copy_template,
|
"copy": copy_template,
|
||||||
"move": move_template,
|
"move": move_template,
|
||||||
|
"metadata": metadata_template,
|
||||||
},
|
},
|
||||||
}) + "\n"
|
}) + "\n"
|
||||||
yield meta_line
|
yield meta_line
|
||||||
@@ -728,11 +726,6 @@ def stream_bucket_objects(bucket_name: str):
|
|||||||
yield json.dumps({"type": "count", "total_count": total_count}) + "\n"
|
yield json.dumps({"type": "count", "total_count": total_count}) + "\n"
|
||||||
|
|
||||||
for obj in result.objects:
|
for obj in result.objects:
|
||||||
metadata = {}
|
|
||||||
try:
|
|
||||||
metadata = storage.get_object_metadata(bucket_name, obj.key)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
yield json.dumps({
|
yield json.dumps({
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"key": obj.key,
|
"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_display": _format_datetime_display(obj.last_modified, display_tz),
|
||||||
"last_modified_iso": _format_datetime_iso(obj.last_modified, display_tz),
|
"last_modified_iso": _format_datetime_iso(obj.last_modified, display_tz),
|
||||||
"etag": obj.etag,
|
"etag": obj.etag,
|
||||||
"metadata": metadata,
|
|
||||||
}) + "\n"
|
}) + "\n"
|
||||||
|
|
||||||
if not result.is_truncated:
|
if not result.is_truncated:
|
||||||
@@ -1181,6 +1173,20 @@ def object_presign(bucket_name: str, object_key: str):
|
|||||||
return jsonify(body), response.status_code
|
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")
|
@ui_bp.get("/buckets/<bucket_name>/objects/<path:object_key>/versions")
|
||||||
def object_versions(bucket_name: str, object_key: str):
|
def object_versions(bucket_name: str, object_key: str):
|
||||||
principal = _current_principal()
|
principal = _current_principal()
|
||||||
|
|||||||
@@ -100,6 +100,7 @@
|
|||||||
const previewPlaceholder = document.getElementById('preview-placeholder');
|
const previewPlaceholder = document.getElementById('preview-placeholder');
|
||||||
const previewImage = document.getElementById('preview-image');
|
const previewImage = document.getElementById('preview-image');
|
||||||
const previewVideo = document.getElementById('preview-video');
|
const previewVideo = document.getElementById('preview-video');
|
||||||
|
const previewAudio = document.getElementById('preview-audio');
|
||||||
const previewIframe = document.getElementById('preview-iframe');
|
const previewIframe = document.getElementById('preview-iframe');
|
||||||
const downloadButton = document.getElementById('downloadButton');
|
const downloadButton = document.getElementById('downloadButton');
|
||||||
const presignButton = document.getElementById('presignButton');
|
const presignButton = document.getElementById('presignButton');
|
||||||
@@ -186,20 +187,20 @@
|
|||||||
tr.dataset.objectRow = '';
|
tr.dataset.objectRow = '';
|
||||||
tr.dataset.key = obj.key;
|
tr.dataset.key = obj.key;
|
||||||
tr.dataset.size = obj.size;
|
tr.dataset.size = obj.size;
|
||||||
tr.dataset.lastModified = obj.lastModified || obj.last_modified;
|
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.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.lastModifiedIso = obj.lastModifiedIso ?? obj.last_modified_iso ?? obj.lastModified ?? obj.last_modified ?? '';
|
||||||
tr.dataset.etag = obj.etag;
|
tr.dataset.etag = obj.etag ?? '';
|
||||||
tr.dataset.previewUrl = obj.previewUrl || obj.preview_url;
|
tr.dataset.previewUrl = obj.previewUrl ?? obj.preview_url ?? '';
|
||||||
tr.dataset.downloadUrl = obj.downloadUrl || obj.download_url;
|
tr.dataset.downloadUrl = obj.downloadUrl ?? obj.download_url ?? '';
|
||||||
tr.dataset.presignEndpoint = obj.presignEndpoint || obj.presign_endpoint;
|
tr.dataset.presignEndpoint = obj.presignEndpoint ?? obj.presign_endpoint ?? '';
|
||||||
tr.dataset.deleteEndpoint = obj.deleteEndpoint || obj.delete_endpoint;
|
tr.dataset.deleteEndpoint = obj.deleteEndpoint ?? obj.delete_endpoint ?? '';
|
||||||
tr.dataset.metadata = typeof obj.metadata === 'string' ? obj.metadata : JSON.stringify(obj.metadata || {});
|
tr.dataset.metadataUrl = obj.metadataUrl ?? obj.metadata_url ?? '';
|
||||||
tr.dataset.versionsEndpoint = obj.versionsEndpoint || obj.versions_endpoint;
|
tr.dataset.versionsEndpoint = obj.versionsEndpoint ?? obj.versions_endpoint ?? '';
|
||||||
tr.dataset.restoreTemplate = obj.restoreTemplate || obj.restore_template;
|
tr.dataset.restoreTemplate = obj.restoreTemplate ?? obj.restore_template ?? '';
|
||||||
tr.dataset.tagsUrl = obj.tagsUrl || obj.tags_url;
|
tr.dataset.tagsUrl = obj.tagsUrl ?? obj.tags_url ?? '';
|
||||||
tr.dataset.copyUrl = obj.copyUrl || obj.copy_url;
|
tr.dataset.copyUrl = obj.copyUrl ?? obj.copy_url ?? '';
|
||||||
tr.dataset.moveUrl = obj.moveUrl || obj.move_url;
|
tr.dataset.moveUrl = obj.moveUrl ?? obj.move_url ?? '';
|
||||||
|
|
||||||
const keyToShow = displayKey || obj.key;
|
const keyToShow = displayKey || obj.key;
|
||||||
const lastModDisplay = obj.lastModifiedDisplay || obj.last_modified_display || new Date(obj.lastModified || obj.last_modified).toLocaleDateString();
|
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) : '',
|
downloadUrl: urlTemplates ? buildUrlFromTemplate(urlTemplates.download, key) : '',
|
||||||
presignEndpoint: urlTemplates ? buildUrlFromTemplate(urlTemplates.presign, key) : '',
|
presignEndpoint: urlTemplates ? buildUrlFromTemplate(urlTemplates.presign, key) : '',
|
||||||
deleteEndpoint: urlTemplates ? buildUrlFromTemplate(urlTemplates.delete, key) : '',
|
deleteEndpoint: urlTemplates ? buildUrlFromTemplate(urlTemplates.delete, key) : '',
|
||||||
metadata: '{}',
|
metadataUrl: urlTemplates ? buildUrlFromTemplate(urlTemplates.metadata, key) : '',
|
||||||
versionsEndpoint: urlTemplates ? buildUrlFromTemplate(urlTemplates.versions, key) : '',
|
versionsEndpoint: urlTemplates ? buildUrlFromTemplate(urlTemplates.versions, key) : '',
|
||||||
restoreTemplate: urlTemplates ? urlTemplates.restore.replace('KEY_PLACEHOLDER', encodeURIComponent(key).replace(/%2F/g, '/')) : '',
|
restoreTemplate: urlTemplates ? urlTemplates.restore.replace('KEY_PLACEHOLDER', encodeURIComponent(key).replace(/%2F/g, '/')) : '',
|
||||||
tagsUrl: urlTemplates ? buildUrlFromTemplate(urlTemplates.tags, key) : '',
|
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) => {
|
const renderMetadata = (metadata) => {
|
||||||
if (!previewMetadata || !previewMetadataList) return;
|
if (!previewMetadata || !previewMetadataList) return;
|
||||||
previewMetadataList.innerHTML = '';
|
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');
|
previewMetadata.classList.add('d-none');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
previewMetadata.classList.remove('d-none');
|
previewMetadata.classList.remove('d-none');
|
||||||
Object.entries(metadata).forEach(([key, value]) => {
|
userMetadata.forEach(([key, value]) => {
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.className = 'metadata-entry';
|
wrapper.className = 'metadata-entry';
|
||||||
const label = document.createElement('div');
|
const label = document.createElement('div');
|
||||||
@@ -1811,9 +1827,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const resetPreviewMedia = () => {
|
const resetPreviewMedia = () => {
|
||||||
[previewImage, previewVideo, previewIframe].forEach((el) => {
|
[previewImage, previewVideo, previewAudio, previewIframe].forEach((el) => {
|
||||||
|
if (!el) return;
|
||||||
el.classList.add('d-none');
|
el.classList.add('d-none');
|
||||||
if (el.tagName === 'VIDEO') {
|
if (el.tagName === 'VIDEO' || el.tagName === 'AUDIO') {
|
||||||
el.pause();
|
el.pause();
|
||||||
el.removeAttribute('src');
|
el.removeAttribute('src');
|
||||||
}
|
}
|
||||||
@@ -1824,28 +1841,27 @@
|
|||||||
previewPlaceholder.classList.remove('d-none');
|
previewPlaceholder.classList.remove('d-none');
|
||||||
};
|
};
|
||||||
|
|
||||||
function metadataFromRow(row) {
|
async function fetchMetadata(metadataUrl) {
|
||||||
if (!row || !row.dataset.metadata) {
|
if (!metadataUrl) return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(row.dataset.metadata);
|
const resp = await fetch(metadataUrl);
|
||||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
if (resp.ok) {
|
||||||
return parsed;
|
const data = await resp.json();
|
||||||
|
return data.metadata || {};
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (e) {
|
||||||
console.warn('Failed to parse metadata for row', err);
|
console.warn('Failed to load metadata', e);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectRow(row) {
|
async function selectRow(row) {
|
||||||
document.querySelectorAll('[data-object-row]').forEach((r) => r.classList.remove('table-active'));
|
document.querySelectorAll('[data-object-row]').forEach((r) => r.classList.remove('table-active'));
|
||||||
row.classList.add('table-active');
|
row.classList.add('table-active');
|
||||||
previewEmpty.classList.add('d-none');
|
previewEmpty.classList.add('d-none');
|
||||||
previewPanel.classList.remove('d-none');
|
previewPanel.classList.remove('d-none');
|
||||||
activeRow = row;
|
activeRow = row;
|
||||||
renderMetadata(metadataFromRow(row));
|
renderMetadata(null);
|
||||||
|
|
||||||
previewKey.textContent = row.dataset.key;
|
previewKey.textContent = row.dataset.key;
|
||||||
previewSize.textContent = formatBytes(Number(row.dataset.size));
|
previewSize.textContent = formatBytes(Number(row.dataset.size));
|
||||||
@@ -1868,18 +1884,36 @@
|
|||||||
resetPreviewMedia();
|
resetPreviewMedia();
|
||||||
const previewUrl = row.dataset.previewUrl;
|
const previewUrl = row.dataset.previewUrl;
|
||||||
const lower = row.dataset.key.toLowerCase();
|
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.src = previewUrl;
|
||||||
previewImage.classList.remove('d-none');
|
previewImage.classList.remove('d-none');
|
||||||
previewPlaceholder.classList.add('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.src = previewUrl;
|
||||||
previewVideo.classList.remove('d-none');
|
previewVideo.classList.remove('d-none');
|
||||||
previewPlaceholder.classList.add('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.src = previewUrl;
|
||||||
|
previewIframe.style.minHeight = '500px';
|
||||||
previewIframe.classList.remove('d-none');
|
previewIframe.classList.remove('d-none');
|
||||||
previewPlaceholder.classList.add('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;
|
const originalSelectRow = selectRow;
|
||||||
selectRow = (row) => {
|
selectRow = async (row) => {
|
||||||
originalSelectRow(row);
|
await originalSelectRow(row);
|
||||||
loadObjectTags(row);
|
loadObjectTags(row);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -320,6 +320,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<img id="preview-image" class="img-fluid d-none w-100" alt="Object preview" style="display: block;" />
|
<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>
|
<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>
|
<iframe id="preview-iframe" class="w-100 d-none" loading="lazy" style="min-height: 200px;"></iframe>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user