mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-22 03:57:47 -04:00
(admin) storage inspector
This commit is contained in:
parent
e2efc1b980
commit
3b457ae676
18
components/admin/AdminStorageItem.vue
Normal file
18
components/admin/AdminStorageItem.vue
Normal 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>
|
58
components/admin/AdminStorageTree.vue
Normal file
58
components/admin/AdminStorageTree.vue
Normal 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
55
pages/admin/storage.vue
Normal 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>
|
22
server/api/admin/storage/metas.get.ts
Normal file
22
server/api/admin/storage/metas.get.ts
Normal 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,
|
||||
};
|
||||
}));
|
||||
});
|
@ -563,6 +563,16 @@ export const filterObjectKeys = <T extends Record<string, any>, K extends keyof
|
||||
}, {} 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) => {
|
||||
return () => {
|
||||
if ((document as any).prerendering) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user