mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-22 12:03:25 -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>);
|
}, {} 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) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user