(admin) storage inspector

This commit is contained in:
Valentyne Stigloher 2025-02-22 14:43:14 +01:00
parent e2efc1b980
commit 3b457ae676
5 changed files with 163 additions and 0 deletions

View File

@ -0,0 +1,18 @@
<script setup lang="ts">
import { formatSize } from '~/src/helpers.ts';
defineProps<{
chunk: string;
node: { size: number; atime: string; mtime: string };
}>();
</script>
<template>
<div class="py-1 ps-3 d-flex justify-content-between">
<code>{{ chunk }}</code>
<span>
<span class="badge text-bg-info">{{ formatSize(node.size) }}</span>
<span class="badge text-bg-light">{{ node.atime }}</span>
</span>
</div>
</template>

View File

@ -0,0 +1,58 @@
<script setup lang="ts">
import { formatSize } from '~/src/helpers.ts';
type Tree = Map<string, Tree | { size: number; atime: string; mtime: string }>;
defineProps<{
chunk: string;
node: Tree;
}>();
const sumSize = (tree: Tree): number => {
return [...tree.values()].map((node) => {
if (node instanceof Map) {
return sumSize(node);
}
return node.size;
})
.reduce((sum, size) => sum + size, 0);
};
const minDate = (tree: Tree): Date => {
return [...tree.values()].map((node) => {
if (node instanceof Map) {
return minDate(node);
}
return new Date(node.atime);
})
.reduce((minDate, date) => minDate.getTime() <= date.getTime() ? minDate : date, new Date());
};
</script>
<template>
<details>
<summary>
<span class="py-1 d-inline-flex justify-content-between">
<code>{{ chunk }}</code>
<span>
<span class="badge text-bg-info">{{ formatSize(sumSize(node)) }}</span>
<span class="badge text-bg-light">{{ minDate(node).toISOString() }}</span>
</span>
</span>
</summary>
<div class="ps-2 border-start border-secondary">
<template v-for="[childChunk, childNode] of node.entries()" :key="childNode">
<AdminStorageTree v-if="childNode instanceof Map" :chunk="childChunk" :node="childNode" />
<AdminStorageItem v-else :chunk="childChunk" :node="childNode" />
</template>
</div>
</details>
</template>
<style scoped lang="scss">
@import 'assets/variables';
summary > span {
width: calc(100% - #{$spacer});
}
</style>

55
pages/admin/storage.vue Normal file
View File

@ -0,0 +1,55 @@
<script setup lang="ts">
import AdminStorageTree from '~/components/admin/AdminStorageTree.vue';
const metasAsyncData = useFetch('/api/admin/storage/metas', { lazy: true });
const tree = computed(() => {
const tree = new Map();
for (const meta of metasAsyncData.data.value ?? []) {
const path = meta.key.split(':');
let subtree = tree;
for (const chunk of path.slice(0, path.length - 1)) {
if (!subtree.has(chunk)) {
subtree.set(chunk, new Map());
}
subtree = subtree.get(chunk);
}
subtree.set(path[path.length - 1], meta);
}
return tree;
});
</script>
<template>
<Page>
<NotFound v-if="!$isGranted('code')" />
<template v-else>
<p>
<nuxt-link to="/admin">
<Icon v="user-cog" />
<T>admin.header</T>
</nuxt-link>
</p>
<h2>
<Icon v="layer-group" />
Storage
</h2>
<p>
Gives an overview about mounted storages.
</p>
<button type="button" class="btn btn-outline-secondary" @click="metasAsyncData.execute()">
<Icon v="sync" />
Refresh
</button>
<Loading :value="metasAsyncData.data.value">
<template v-for="[chunk, node] of tree.entries()" :key="chunk">
<AdminStorageTree v-if="node instanceof Map" :chunk :node />
<AdminStorageItem v-else :chunk :node />
</template>
</Loading>
</template>
</Page>
</template>

View File

@ -0,0 +1,22 @@
export default defineEventHandler(async (event) => {
const { isGranted } = await useAuthentication(event);
if (!isGranted('code')) {
throw createError({
status: 401,
statusMessage: 'Unauthorised',
});
}
const keys = (await useStorage().getKeys())
.filter((key) => !key.startsWith('build') && !key.startsWith('root') && !key.startsWith('src'))
.toSorted();
return await Promise.all(keys.map(async (key) => {
const meta = await useStorage().getMeta(key);
return {
key,
size: meta.size,
atime: meta.atime,
mtime: meta.mtime,
};
}));
});

View File

@ -563,6 +563,16 @@ export const filterObjectKeys = <T extends Record<string, any>, K extends keyof
}, {} as Pick<T, K>); }, {} as Pick<T, K>);
}; };
export const formatSize = (number: number): string => {
if (number > 1000000) {
return `${Math.round(10 * number / 1000000) / 10}\u00a0MB`;
}
if (number > 1000) {
return `${Math.round(10 * number / 1000) / 10}\u00a0kB`;
}
return number.toString();
};
export const executeUnlessPrerendering = (fn: () => void): (() => void) => { export const executeUnlessPrerendering = (fn: () => void): (() => void) => {
return () => { return () => {
if ((document as any).prerendering) { if ((document as any).prerendering) {