update storage display method

This commit is contained in:
Kohaku-Blueleaf
2025-10-18 18:29:41 +08:00
parent bd465ceeea
commit a7d1cbfdcd
3 changed files with 82 additions and 160 deletions

View File

@@ -10,11 +10,8 @@ import dayjs from "dayjs";
const router = useRouter();
const adminStore = useAdminStore();
const buckets = ref([]);
const objects = ref([]);
const loadingBuckets = ref(false);
const loadingObjects = ref(false);
const selectedBucket = ref(null);
const loading = ref(false);
const currentPath = ref("");
const pathParts = ref([]);
@@ -64,34 +61,10 @@ function checkAuth() {
return true;
}
async function loadBuckets() {
async function loadObjects(prefix = "") {
if (!checkAuth()) return;
loadingBuckets.value = true;
try {
const response = await listS3Buckets(adminStore.token);
buckets.value = response.buckets;
} catch (error) {
console.error("Failed to load buckets:", error);
if (error.response?.status === 401 || error.response?.status === 403) {
ElMessage.error("Invalid admin token. Please login again.");
adminStore.logout();
router.push("/login");
} else {
ElMessage.error(
error.response?.data?.detail?.error || "Failed to load S3 buckets",
);
}
} finally {
loadingBuckets.value = false;
}
}
async function loadObjects(bucket, prefix = "") {
if (!checkAuth()) return;
loadingObjects.value = true;
selectedBucket.value = bucket;
loading.value = true;
currentPath.value = prefix;
// Update breadcrumb path parts
@@ -102,7 +75,8 @@ async function loadObjects(bucket, prefix = "") {
}
try {
const response = await listS3Objects(adminStore.token, bucket.name, {
// Just list objects from the default configured bucket
const response = await listS3Objects(adminStore.token, "", {
prefix,
limit: 1000,
});
@@ -110,33 +84,33 @@ async function loadObjects(bucket, prefix = "") {
} catch (error) {
console.error("Failed to load objects:", error);
ElMessage.error(
error.response?.data?.detail?.error || "Failed to load S3 objects",
error.response?.data?.detail?.error || "Failed to load storage objects",
);
} finally {
loadingObjects.value = false;
loading.value = false;
}
}
function navigateToFolder(folderName) {
const newPath = currentPath.value + folderName + "/";
loadObjects(selectedBucket.value, newPath);
loadObjects(newPath);
}
function navigateToBreadcrumb(index) {
if (index === -1) {
// Navigate to bucket root
loadObjects(selectedBucket.value, "");
// Navigate to root
loadObjects("");
} else {
// Navigate to specific path level
const newPath = pathParts.value.slice(0, index + 1).join("/") + "/";
loadObjects(selectedBucket.value, newPath);
loadObjects(newPath);
}
}
function navigateUp() {
if (pathParts.value.length === 0) {
// Already at root, go back to buckets
clearObjectView();
// Already at root
return;
} else {
// Go up one level
navigateToBreadcrumb(pathParts.value.length - 2);
@@ -147,32 +121,6 @@ function formatDate(dateStr) {
return dayjs(dateStr).format("YYYY-MM-DD HH:mm:ss");
}
function getBucketProgress(bucket) {
// Return percentage for visual display
if (!bucket.total_size) return 0;
// Max 100GB for visual purposes
const maxSize = 100 * 1000 * 1000 * 1000;
return Math.min((bucket.total_size / maxSize) * 100, 100);
}
function getBucketColor(bucket) {
const progress = getBucketProgress(bucket);
if (progress > 80) return "danger";
if (progress > 50) return "warning";
return "success";
}
function handleBrowseBucket(bucket) {
loadObjects(bucket, "");
}
function clearObjectView() {
selectedBucket.value = null;
objects.value = [];
currentPath.value = "";
pathParts.value = [];
}
function getFileIcon(fileName) {
const ext = fileName.split(".").pop().toLowerCase();
const iconMap = {
@@ -200,7 +148,7 @@ function getFileIcon(fileName) {
}
onMounted(() => {
loadBuckets();
loadObjects(""); // Load root directly
});
</script>
@@ -209,94 +157,26 @@ onMounted(() => {
<div class="page-container">
<div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100">
S3 Storage Browser
Storage Browser
</h1>
<el-button @click="loadBuckets" :icon="'Refresh'">Refresh</el-button>
<el-button @click="loadObjects('')" :icon="'Refresh'" :loading="loading">
Refresh
</el-button>
</div>
<!-- Buckets Overview -->
<el-card class="mb-4">
<template #header>
<div class="flex items-center justify-between">
<span class="font-bold">S3 Buckets</span>
<span class="text-sm text-gray-500">
{{ buckets.length }} bucket(s)
</span>
</div>
</template>
<el-empty
v-if="!loadingBuckets && buckets.length === 0"
description="No buckets found"
/>
<div
v-else
v-loading="loadingBuckets"
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"
>
<div
v-for="bucket in buckets"
:key="bucket.name"
class="bucket-card p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:shadow-md transition-shadow cursor-pointer"
@click="handleBrowseBucket(bucket)"
>
<div class="flex items-start justify-between mb-2">
<div class="flex items-center gap-2">
<div class="i-carbon-data-base text-2xl text-blue-500" />
<div>
<h3 class="font-bold text-gray-900 dark:text-gray-100">
{{ bucket.name }}
</h3>
<p class="text-xs text-gray-500">
{{ formatDate(bucket.creation_date) }}
</p>
</div>
</div>
</div>
<div class="mt-3">
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-600 dark:text-gray-400">Size:</span>
<span class="font-semibold">{{
formatBytes(bucket.total_size)
}}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-gray-600 dark:text-gray-400">Objects:</span>
<span class="font-semibold">{{
bucket.object_count.toLocaleString()
}}</span>
</div>
</div>
<el-progress
:percentage="getBucketProgress(bucket)"
:color="getBucketColor(bucket)"
:stroke-width="6"
class="mt-3"
/>
<div v-if="bucket.error" class="mt-2 text-xs text-red-500">
Error: {{ bucket.error }}
</div>
</div>
</div>
</el-card>
<!-- File Explorer -->
<el-card v-if="selectedBucket">
<!-- File Explorer (Direct) -->
<el-card>
<template #header>
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<el-button size="small" @click="clearObjectView" :icon="'Back'">
Back to Buckets
</el-button>
<span class="font-bold">{{ selectedBucket.name }}</span>
<div class="i-carbon-folder-open text-2xl text-blue-600" />
<span class="font-bold">Storage Explorer</span>
</div>
<div class="flex items-center gap-2 text-sm text-gray-500">
<div class="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
<span>{{ folderStructure.folders.length }} folders</span>
<span></span>
<span>{{ folderStructure.files.length }} files</span>
<span></span>
<span>{{ objects.length }} total objects</span>
</div>
</div>
@@ -309,7 +189,7 @@ onMounted(() => {
@click="navigateUp"
:icon="'ArrowLeft'"
class="mr-2"
:disabled="!selectedBucket"
:disabled="pathParts.length === 0"
>
Up
</el-button>
@@ -335,10 +215,10 @@ onMounted(() => {
</div>
<!-- File Explorer View -->
<div v-loading="loadingObjects">
<div v-loading="loading">
<el-empty
v-if="!loadingObjects && objects.length === 0"
description="This bucket is empty"
v-if="!loading && objects.length === 0"
description="Storage is empty"
/>
<div v-else class="explorer-container">

View File

@@ -349,17 +349,19 @@ export async function listS3Buckets(token) {
/**
* List S3 objects in a bucket
* @param {string} token - Admin token
* @param {string} bucket - Bucket name
* @param {string} bucket - Bucket name (empty = use configured bucket)
* @param {Object} params - Query parameters
* @returns {Promise<Object>} Object list
*/
export async function listS3Objects(
token,
bucket,
{ prefix = "", limit = 100 } = {},
{ prefix = "", limit = 1000 } = {},
) {
const client = createAdminClient(token);
const response = await client.get(`/storage/objects/${bucket}`, {
// Use /storage/objects (no bucket) to use configured bucket
const url = bucket ? `/storage/objects/${bucket}` : "/storage/objects";
const response = await client.get(url, {
params: { prefix, limit },
});
return response.data;

View File

@@ -26,10 +26,44 @@ async def list_s3_buckets(
def _list_buckets():
s3 = get_s3_client()
buckets = s3.list_buckets()
try:
buckets = s3.list_buckets()
logger.info(f"list_buckets() response: {buckets}")
except Exception as e:
logger.error(f"Failed to list buckets: {e}")
# For R2/path-style, list_buckets might not work
# Return configured bucket as fallback
from kohakuhub.config import cfg
return [
{
"name": cfg.s3.bucket,
"creation_date": "N/A",
"total_size": 0,
"object_count": 0,
"note": "Using configured bucket (list_buckets not supported)",
}
]
bucket_list = buckets.get("Buckets", [])
logger.info(f"Found {len(bucket_list)} buckets")
# If no buckets returned (R2 path-style issue), use configured bucket
if not bucket_list:
from kohakuhub.config import cfg
logger.warning("list_buckets returned empty, using configured bucket")
return [
{
"name": cfg.s3.bucket,
"creation_date": "N/A",
"total_size": 0,
"object_count": 0,
"note": "Using configured bucket",
}
]
bucket_info = []
for bucket in buckets.get("Buckets", []):
for bucket in bucket_list:
bucket_name = bucket["Name"]
# Get bucket size (sum of all objects)
@@ -72,17 +106,18 @@ async def list_s3_buckets(
return {"buckets": buckets}
@router.get("/storage/objects")
@router.get("/storage/objects/{bucket}")
async def list_s3_objects(
bucket: str,
bucket: str = "",
prefix: str = "",
limit: int = 100,
limit: int = 1000,
_admin: bool = Depends(verify_admin_token),
):
"""List S3 objects in a bucket.
"""List S3 objects in configured bucket or specified bucket.
Args:
bucket: Bucket name
bucket: Bucket name (empty = use configured bucket)
prefix: Key prefix filter
limit: Maximum objects to return
_admin: Admin authentication (dependency)
@@ -90,12 +125,16 @@ async def list_s3_objects(
Returns:
List of S3 objects
"""
from kohakuhub.config import cfg
# Use configured bucket if not specified
bucket_name = bucket if bucket else cfg.s3.bucket
def _list_objects():
s3 = get_s3_client()
try:
response = s3.list_objects_v2(Bucket=bucket, Prefix=prefix, MaxKeys=limit)
response = s3.list_objects_v2(Bucket=bucket_name, Prefix=prefix, MaxKeys=limit)
objects = []
for obj in response.get("Contents", []):
@@ -110,11 +149,12 @@ async def list_s3_objects(
return {
"objects": objects,
"bucket": bucket_name,
"is_truncated": response.get("IsTruncated", False),
"key_count": len(objects),
}
except Exception as e:
logger.error(f"Failed to list objects in bucket {bucket}: {e}")
logger.error(f"Failed to list objects in bucket {bucket_name}: {e}")
raise HTTPException(500, detail={"error": str(e)})
result = await run_in_s3_executor(_list_objects)