MyFSIO v0.2.2 Release #14

Merged
kqjy merged 17 commits from next into main 2026-01-19 07:12:15 +00:00
5 changed files with 75 additions and 8 deletions
Showing only changes of commit a3b9db544c - Show all commits

View File

@@ -1366,7 +1366,7 @@ def _bucket_list_versions_handler(bucket_name: str) -> Response:
SubElement(ver_elem, "Key").text = obj.key SubElement(ver_elem, "Key").text = obj.key
SubElement(ver_elem, "VersionId").text = v.get("version_id", "unknown") SubElement(ver_elem, "VersionId").text = v.get("version_id", "unknown")
SubElement(ver_elem, "IsLatest").text = "false" 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, "ETag").text = f'"{v.get("etag", "")}"'
SubElement(ver_elem, "Size").text = str(v.get("size", 0)) SubElement(ver_elem, "Size").text = str(v.get("size", 0))
SubElement(ver_elem, "StorageClass").text = "STANDARD" SubElement(ver_elem, "StorageClass").text = "STANDARD"

View File

@@ -774,7 +774,7 @@ class ObjectStorage:
continue continue
payload.setdefault("version_id", meta_file.stem) payload.setdefault("version_id", meta_file.stem)
versions.append(payload) 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 return versions
def restore_object_version(self, bucket_name: str, object_key: str, version_id: str) -> ObjectMeta: 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): except (OSError, json.JSONDecodeError):
payload = {} payload = {}
version_id = payload.get("version_id") or meta_file.stem 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) size = int(payload.get("size") or 0)
reason = payload.get("reason") or "update" reason = payload.get("reason") or "update"
record = aggregated.setdefault( record = aggregated.setdefault(

View File

@@ -69,7 +69,8 @@ def _format_datetime_display(dt: datetime, display_tz: str | None = None) -> str
display_tz: Optional timezone string. If not provided, reads from current_app.config. display_tz: Optional timezone string. If not provided, reads from current_app.config.
""" """
dt = _convert_to_display_tz(dt, display_tz) 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: 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 = [] 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,
@@ -565,6 +571,7 @@ 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({
@@ -657,6 +664,11 @@ 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,
@@ -665,6 +677,7 @@ 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:

View File

@@ -28,6 +28,57 @@
setupJsonAutoIndent(document.getElementById('policyDocument')); 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: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="text-success flex-shrink-0" viewBox="0 0 16 16">
<path d="M6.002 5.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/>
<path d="M2.002 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2h-12zm12 1a1 1 0 0 1 1 1v6.5l-3.777-1.947a.5.5 0 0 0-.577.093l-3.71 3.71-2.66-1.772a.5.5 0 0 0-.63.062L1.002 12V3a1 1 0 0 1 1-1h12z"/>
</svg>`,
document: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="text-danger flex-shrink-0" viewBox="0 0 16 16">
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/>
<path d="M4.5 12.5A.5.5 0 0 1 5 12h3a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm0-2A.5.5 0 0 1 5 10h6a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm0-2A.5.5 0 0 1 5 8h6a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm0-2A.5.5 0 0 1 5 6h6a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5z"/>
</svg>`,
spreadsheet: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="text-success flex-shrink-0" viewBox="0 0 16 16">
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2zM9.5 3A1.5 1.5 0 0 0 11 4.5h2V9H3V2a1 1 0 0 1 1-1h5.5v2zM3 12v-2h2v2H3zm0 1h2v2H4a1 1 0 0 1-1-1v-1zm3 2v-2h3v2H6zm4 0v-2h3v1a1 1 0 0 1-1 1h-2zm3-3h-3v-2h3v2zm-7 0v-2h3v2H6z"/>
</svg>`,
archive: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="text-secondary flex-shrink-0" viewBox="0 0 16 16">
<path d="M6.5 7.5a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1v.938l.4 1.599a1 1 0 0 1-.416 1.074l-.93.62a1 1 0 0 1-1.109 0l-.93-.62a1 1 0 0 1-.415-1.074l.4-1.599V7.5z"/>
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1h-2v1h-1v1h1v1h-1v1h1v1H6V5H5V4h1V3H5V2h1V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/>
</svg>`,
code: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="text-info flex-shrink-0" viewBox="0 0 16 16">
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/>
<path d="M8.646 6.646a.5.5 0 0 1 .708 0l2 2a.5.5 0 0 1 0 .708l-2 2a.5.5 0 0 1-.708-.708L10.293 9 8.646 7.354a.5.5 0 0 1 0-.708zm-1.292 0a.5.5 0 0 0-.708 0l-2 2a.5.5 0 0 0 0 .708l2 2a.5.5 0 0 0 .708-.708L5.707 9l1.647-1.646a.5.5 0 0 0 0-.708z"/>
</svg>`,
audio: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="text-primary flex-shrink-0" viewBox="0 0 16 16">
<path d="M6 13c0 1.105-1.12 2-2.5 2S1 14.105 1 13c0-1.104 1.12-2 2.5-2s2.5.896 2.5 2zm9-2c0 1.105-1.12 2-2.5 2s-2.5-.895-2.5-2 1.12-2 2.5-2 2.5.895 2.5 2z"/>
<path fill-rule="evenodd" d="M14 11V2h1v9h-1zM6 3v10H5V3h1z"/>
<path d="M5 2.905a1 1 0 0 1 .9-.995l8-.8a1 1 0 0 1 1.1.995V3L5 4V2.905z"/>
</svg>`,
video: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="text-danger flex-shrink-0" viewBox="0 0 16 16">
<path d="M0 12V4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm6.79-6.907A.5.5 0 0 0 6 5.5v5a.5.5 0 0 0 .79.407l3.5-2.5a.5.5 0 0 0 0-.814l-3.5-2.5z"/>
</svg>`,
default: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="text-muted flex-shrink-0" viewBox="0 0 16 16">
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/>
</svg>`,
};
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 selectAllCheckbox = document.querySelector('[data-select-all]');
const bulkDeleteButton = document.querySelector('[data-bulk-delete-trigger]'); const bulkDeleteButton = document.querySelector('[data-bulk-delete-trigger]');
const bulkDeleteLabel = bulkDeleteButton?.querySelector('[data-bulk-delete-label]'); const bulkDeleteLabel = bulkDeleteButton?.querySelector('[data-bulk-delete-label]');
@@ -158,8 +209,11 @@
<input class="form-check-input" type="checkbox" data-object-select aria-label="Select ${escapeHtml(obj.key)}" /> <input class="form-check-input" type="checkbox" data-object-select aria-label="Select ${escapeHtml(obj.key)}" />
</td> </td>
<td class="object-key text-break" title="${escapeHtml(obj.key)}"> <td class="object-key text-break" title="${escapeHtml(obj.key)}">
<div class="fw-medium">${escapeHtml(keyToShow)}</div> <div class="fw-medium d-flex align-items-center gap-2">
<div class="text-muted small">Modified ${escapeHtml(lastModDisplay)}</div> ${getFileTypeIcon(obj.key)}
<span>${escapeHtml(keyToShow)}</span>
</div>
<div class="text-muted small ms-4 ps-2">Modified ${escapeHtml(lastModDisplay)}</div>
</td> </td>
<td class="text-end text-nowrap"> <td class="text-end text-nowrap">
<span class="text-muted small">${formatBytes(obj.size)}</span> <span class="text-muted small">${formatBytes(obj.size)}</span>

View File

@@ -51,7 +51,7 @@
</div> </div>
<div> <div>
<h5 class="bucket-name text-break">{{ bucket.meta.name }}</h5> <h5 class="bucket-name text-break">{{ bucket.meta.name }}</h5>
<small class="text-muted">Created {{ bucket.meta.created_at.strftime('%b %d, %Y') }}</small> <small class="text-muted">Created {{ bucket.meta.created_at.strftime('%b %d, %Y %H:%M') }} ({{ bucket.meta.created_at.strftime('%Z') or 'UTC' }})</small>
</div> </div>
</div> </div>
<span class="badge {{ bucket.access_badge }} bucket-access-badge">{{ bucket.access_label }}</span> <span class="badge {{ bucket.access_badge }} bucket-access-badge">{{ bucket.access_label }}</span>