mirror of
https://github.com/KohakuBlueleaf/KohakuHub.git
synced 2026-03-11 17:34:08 -05:00
update storage display method
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user