diff --git a/app/s3_api.py b/app/s3_api.py index e236c98..20fd821 100644 --- a/app/s3_api.py +++ b/app/s3_api.py @@ -1366,7 +1366,7 @@ def _bucket_list_versions_handler(bucket_name: str) -> Response: SubElement(ver_elem, "Key").text = obj.key SubElement(ver_elem, "VersionId").text = v.get("version_id", "unknown") SubElement(ver_elem, "IsLatest").text = "false" - SubElement(ver_elem, "LastModified").text = v.get("archived_at", "") + SubElement(ver_elem, "LastModified").text = v.get("archived_at") or "1970-01-01T00:00:00Z" SubElement(ver_elem, "ETag").text = f'"{v.get("etag", "")}"' SubElement(ver_elem, "Size").text = str(v.get("size", 0)) SubElement(ver_elem, "StorageClass").text = "STANDARD" diff --git a/app/storage.py b/app/storage.py index d37279b..b5e38be 100644 --- a/app/storage.py +++ b/app/storage.py @@ -774,7 +774,7 @@ class ObjectStorage: continue payload.setdefault("version_id", meta_file.stem) versions.append(payload) - versions.sort(key=lambda item: item.get("archived_at", ""), reverse=True) + versions.sort(key=lambda item: item.get("archived_at") or "1970-01-01T00:00:00Z", reverse=True) return versions def restore_object_version(self, bucket_name: str, object_key: str, version_id: str) -> ObjectMeta: @@ -866,7 +866,7 @@ class ObjectStorage: except (OSError, json.JSONDecodeError): payload = {} version_id = payload.get("version_id") or meta_file.stem - archived_at = payload.get("archived_at") or "" + archived_at = payload.get("archived_at") or "1970-01-01T00:00:00Z" size = int(payload.get("size") or 0) reason = payload.get("reason") or "update" record = aggregated.setdefault( diff --git a/app/ui.py b/app/ui.py index ab7fb15..28f87f5 100644 --- a/app/ui.py +++ b/app/ui.py @@ -63,13 +63,14 @@ def _convert_to_display_tz(dt: datetime, display_tz: str | None = None) -> datet def _format_datetime_display(dt: datetime, display_tz: str | None = None) -> str: """Format a datetime for display using the configured timezone. - + Args: dt: The datetime to format display_tz: Optional timezone string. If not provided, reads from current_app.config. """ dt = _convert_to_display_tz(dt, display_tz) - return dt.strftime("%b %d, %Y %H:%M") + tz_abbr = dt.strftime("%Z") or "UTC" + return f"{dt.strftime('%b %d, %Y %H:%M')} ({tz_abbr})" def _format_datetime_iso(dt: datetime, display_tz: str | None = None) -> str: @@ -558,6 +559,11 @@ def list_bucket_objects(bucket_name: str): 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, @@ -565,6 +571,7 @@ 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({ @@ -657,6 +664,11 @@ 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, @@ -665,6 +677,7 @@ 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: diff --git a/static/js/bucket-detail-main.js b/static/js/bucket-detail-main.js index 8186de8..3e66b26 100644 --- a/static/js/bucket-detail-main.js +++ b/static/js/bucket-detail-main.js @@ -28,6 +28,57 @@ setupJsonAutoIndent(document.getElementById('policyDocument')); + const getFileTypeIcon = (key) => { + const ext = (key.split('.').pop() || '').toLowerCase(); + const iconMap = { + image: ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'ico', 'bmp', 'tiff', 'tif'], + document: ['pdf', 'doc', 'docx', 'txt', 'rtf', 'odt', 'pages'], + spreadsheet: ['xls', 'xlsx', 'csv', 'ods', 'numbers'], + archive: ['zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', 'tgz'], + code: ['js', 'ts', 'jsx', 'tsx', 'py', 'java', 'cpp', 'c', 'h', 'hpp', 'cs', 'go', 'rs', 'rb', 'php', 'html', 'htm', 'css', 'scss', 'sass', 'less', 'json', 'xml', 'yaml', 'yml', 'md', 'sh', 'bat', 'ps1', 'sql'], + audio: ['mp3', 'wav', 'flac', 'ogg', 'aac', 'm4a', 'wma', 'aiff'], + video: ['mp4', 'avi', 'mov', 'mkv', 'webm', 'wmv', 'flv', 'm4v', 'mpeg', 'mpg'], + }; + const icons = { + image: ``, + document: ``, + spreadsheet: ``, + archive: ``, + code: ``, + audio: ``, + video: ``, + default: ``, + }; + for (const [type, extensions] of Object.entries(iconMap)) { + if (extensions.includes(ext)) { + return icons[type]; + } + } + return icons.default; + }; + const selectAllCheckbox = document.querySelector('[data-select-all]'); const bulkDeleteButton = document.querySelector('[data-bulk-delete-trigger]'); const bulkDeleteLabel = bulkDeleteButton?.querySelector('[data-bulk-delete-label]'); @@ -158,8 +209,11 @@