Merge remote-tracking branch 'origin/main'

This commit is contained in:
Andrea Vos 2025-03-10 20:02:08 +01:00
commit 64cc9de2bc
12 changed files with 208 additions and 42 deletions

3
.gitignore vendored
View File

@ -17,8 +17,7 @@
/locale/*.schema.json
/cache
/public/card
/public/calendar
/calendar
/test/output

View File

@ -38,11 +38,21 @@
</a>
</p>
<p v-else class="mb-0">
<a :href="`/calendar/${year.year}-overview.png`" target="_blank" rel="noopener" class="btn btn-outline-primary m-1">
<a
:href="`/calendar/${config.locale}/${year.year}-overview.png`"
target="_blank"
rel="noopener"
class="btn btn-outline-primary m-1"
>
<Icon v="table" />
<T>calendar.view.grid</T>
</a>
<a :href="`/calendar/${year.year}-labels.png`" target="_blank" rel="noopener" class="btn btn-outline-primary m-1">
<a
:href="`/calendar/${config.locale}/${year.year}-labels.png`"
target="_blank"
rel="noopener"
class="btn btn-outline-primary m-1"
>
<Icon v="list" />
<T>calendar.view.list</T>
</a>

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

@ -30,7 +30,7 @@ const year = route.params.year
if (year) {
useSimpleHead({
title: translator.translate('calendar.headerLong'),
banner: `calendar/${year.year}-overview.png`,
banner: `calendar/${config.locale}/${year.year}-overview.png`,
}, translator);
}

View File

@ -20,6 +20,7 @@ definePageMeta({
const { $translator: translator } = useNuxtApp();
const route = useRoute();
const config = useConfig();
const day = new Day(
parseInt(route.params.year as string),
@ -29,10 +30,9 @@ const day = new Day(
useSimpleHead({
title: translator.translate('calendar.headerLong'),
banner: `calendar/${day}.png`,
banner: `calendar/${config.locale}/${day}.png`,
}, translator);
const config = useConfig();
const year = (await loadCalendar()).getYear(day.year);
const basic = route.query.layout === 'basic';

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

@ -2,19 +2,17 @@ import fs from 'node:fs/promises';
import { createCanvas, loadImage } from 'canvas';
import type { CanvasRenderingContext2D, Image } from 'canvas';
import { Router } from 'express';
import sharp from 'sharp';
import SQL from 'sql-template-strings';
import type { Translations } from '../../locale/translations.ts';
import { buildPronounUsage } from '../../src/buildPronoun.ts';
import type { PronounUsage } from '../../src/classes.ts';
import { handleErrorAsync } from '../../src/helpers.ts';
import { Translator } from '../../src/translator.ts';
import type { User } from '../../src/user.ts';
import avatar from '../avatar.ts';
import { loadSuml, loadSumlFromBase } from '../loader.ts';
import { registerLocaleFont } from '../localeFont.ts';
import type { Translations } from '../../../locale/translations.ts';
import { buildPronounUsage } from '../../../src/buildPronoun.ts';
import type { PronounUsage } from '../../../src/classes.ts';
import { Translator } from '../../../src/translator.ts';
import type { User } from '../../../src/user.ts';
import avatar from '../../avatar.ts';
import { loadSuml, loadSumlFromBase } from '../../loader.ts';
import { registerLocaleFont } from '../../localeFont.ts';
import { pronounLibrary } from '~/server/data.ts';
import type { Database } from '~/server/db.ts';
@ -46,8 +44,6 @@ const loadSvgImage = async (svg: string): Promise<Image> => {
return await loadImage(pngBuffer);
};
const router = Router();
const width = 1200;
const height = 600;
const mime = 'image/png';
@ -142,12 +138,12 @@ const drawProfileBanner = async (
return `user:${user.username}.png`;
};
router.get('/banner/:pronounName*.png', handleErrorAsync(async (req, res) => {
const path = (req.params.pronounName + req.params[0]).replace(/(&amp)+$/, '');
export default defineEventHandler(async (event) => {
const path = (getRouterParam(event, 'path', { decode: true }) ?? '').replace(/(&amp)+$/, '').replace(/\.png$/, '');
const db = useDatabase();
const key = await getBannerKey(path, req.db);
const key = await getBannerKey(path, db);
const banner = await useStorage('data').getItemRaw<Buffer>(`banner:${key}`);
return res.set('content-type', mime).send(banner);
}));
export default router;
appendHeader(event, 'content-type', mime);
return banner;
});

View File

@ -1,6 +1,7 @@
import './setup.ts';
import fs from 'fs';
import path from 'path';
import Pageres from 'pageres';
@ -15,21 +16,21 @@ const __dirname = new URL('.', import.meta.url).pathname;
const config = loadSuml('config') as Config;
const dir = `${__dirname}/../public/calendar`;
const force = process.argv[2] === '-f' || process.argv[2] === '--force';
const shoot = async (url: string, filename: string): Promise<void> => {
const pr = new Pageres({
delay: 3,
scale: 2,
launchOptions: {
headless: 'new',
},
});
pr.source(process.env.NUXT_PUBLIC_BASE_URL + url, ['1500x300']);
for (const buffer of await pr.run()) {
fs.mkdirSync(dir, { recursive: true });
const target = `${dir}/${filename}.png`;
console.log(target);
fs.writeFileSync(target, buffer);
fs.mkdirSync(path.dirname(filename), { recursive: true });
console.log(filename);
fs.writeFileSync(filename, buffer);
}
};
@ -57,19 +58,18 @@ const dumpNameDays = async (): Promise<void> => {
return;
}
const prevPath = `${__dirname}/../cache/calendar.json`;
const dir = `${__dirname}/../calendar/${config.locale}`;
const prevPath = `${dir}/generated.json`;
const prev = fs.existsSync(prevPath) ? JSON.parse(fs.readFileSync(prevPath, 'utf-8')) : {};
const localEvents = (await import(`../locale/${config.locale}/calendar/events.ts`)).default;
const current = buildCalendar(localEvents, process.env.NUXT_PUBLIC_BASE_URL!).buildSummary();
const changedYears = new Set();
for (const day in current) {
if (!Object.hasOwn(current, day)) {
continue;
}
for (const day of Object.keys(current)) {
const year = day.substring(0, 4);
if (current[day] !== prev[day] || !fs.existsSync(`${dir}/${day}.png`) || force) {
await shoot(`/${config.calendar.route}/${day}?layout=basic`, `${day}`);
await shoot(`/${config.calendar.route}/${day}?layout=basic`, `${dir}/${day}.png`);
changedYears.add(year);
}
if (!fs.existsSync(`${dir}/${year}-overview.png`) || !fs.existsSync(`${dir}/${year}-labels.png`) || force) {
@ -78,8 +78,8 @@ const dumpNameDays = async (): Promise<void> => {
}
for (const year of changedYears) {
await shoot(`/${config.calendar.route}/${year}?layout=basic`, `${year}-overview`);
await shoot(`/${config.calendar.route}/${year}?layout=basic&labels=true`, `${year}-labels`);
await shoot(`/${config.calendar.route}/${year}?layout=basic`, `${dir}/${year}-overview.png`);
await shoot(`/${config.calendar.route}/${year}?layout=basic&labels=true`, `${dir}/${year}-labels.png`);
}
fs.writeFileSync(prevPath, JSON.stringify(current, null, 4));

View File

@ -18,7 +18,6 @@ import './globals.ts';
import dbConnection from './db.ts';
import type { Database, SQLQuery } from './db.ts';
import adminRoute from './express/admin.ts';
import bannerRoute from './express/banner.ts';
import calendarRoute from './express/calendar.ts';
import censusRoute from './express/census.ts';
import discord from './express/discord.ts';
@ -145,7 +144,6 @@ router.use(grantOverridesRoute);
router.use(grant.express()(config));
router.use(homeRoute);
router.use(bannerRoute);
router.use(sentryRoute);
if (!global.config.macrolanguage?.enabled) {

View File

@ -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) {