diff --git a/app/const/localization.ts b/app/const/localization.ts index 3d876d6..2d9872e 100644 --- a/app/const/localization.ts +++ b/app/const/localization.ts @@ -79,6 +79,7 @@ export const localization = { mb: 'MB', nameConflict: 'Database with this name already exists. Please delete it first', + finalDatabaseName: 'Final database name:', container: 'Container', noLogsAvailable: 'No logs available for this container', failedToDownload: 'Failed to download', diff --git a/app/lib/databaseNameHelper.ts b/app/lib/databaseNameHelper.ts new file mode 100644 index 0000000..0d699fa --- /dev/null +++ b/app/lib/databaseNameHelper.ts @@ -0,0 +1,11 @@ +/** + * Generate a database name with today's date suffix + * Format: {baseName}_YYYY_MM_DD + */ +export function generateDatabaseNameWithDate(baseName: string): string { + const today = new Date(); + const year = today.getFullYear(); + const month = String(today.getMonth() + 1).padStart(2, '0'); + const day = String(today.getDate()).padStart(2, '0'); + return `${baseName}_${year}_${month}_${day}`; +} diff --git a/app/pages/api/databases/[name]/export.ts b/app/pages/api/databases/[name]/export.ts index 4d423f5..d8d304c 100644 --- a/app/pages/api/databases/[name]/export.ts +++ b/app/pages/api/databases/[name]/export.ts @@ -24,17 +24,12 @@ export default async function handler( const sanitizedFilename = String(databaseName).replace(/[^\w.-]/g, '_'); try { - res.setHeader( - 'Content-Disposition', - `attachment; filename="${sanitizedFilename}.sql"` - ); - - const child = spawn( - 'mysqldump', + const result = await spawn( + 'mariadb-dump', [ - `-u${process.env.MYSQL_USERNAME}`, - `-p${process.env.MYSQL_PASSWORD}`, - `-h${process.env.MYSQL_HOST}`, + `--user=${process.env.MYSQL_USERNAME}`, + `--password=${process.env.MYSQL_PASSWORD}`, + `--host=${process.env.MYSQL_HOST}`, `--databases ${databaseName}`, '--no-create-db', ], @@ -44,11 +39,16 @@ export default async function handler( } ); - child.stdout.pipe(res); - child.stderr.on('data', (error) => { + res.setHeader( + 'Content-Disposition', + `attachment; filename="${sanitizedFilename}.sql"` + ); + + result.stdout.pipe(res); + result.stderr.on('data', (error) => { throw new Error(error); }); - await new Promise((resolve) => child.stdout.on('exit', resolve)); + await new Promise((resolve) => result.stdout.on('exit', resolve)); } catch (error) { res.status(500).json({ error: (error as object).toString(), diff --git a/app/pages/api/databases/upload.ts b/app/pages/api/databases/upload.ts index 1465c3c..aac775c 100644 --- a/app/pages/api/databases/upload.ts +++ b/app/pages/api/databases/upload.ts @@ -6,6 +6,7 @@ import path from 'node:path'; import { getUser, run } from '../../../lib/apiUtils'; import { connectToDatabase } from '../../../lib/database'; +import { generateDatabaseNameWithDate } from '../../../lib/databaseNameHelper'; // First we need to disable the default body parser export const config = { @@ -45,14 +46,16 @@ export default async function handler( if (typeof data === 'string') return res.status(400).json({ error: data }); - const databaseName = data.fields.databaseName as string | undefined; + const databaseNameForm = data.fields.databaseName as string | undefined; - if (!databaseName) + if (!databaseNameForm) return res.status(400).json({ error: 'Database name is required' }); - if (databaseName.match(/^\w+$/) === null) + if (databaseNameForm.match(/^\w+$/) === null) return res.status(400).json({ error: 'Database name is invalid' }); + const databaseName = generateDatabaseNameWithDate(databaseNameForm); + const file = data.files.file as File | undefined; if (typeof file === 'undefined' || file.newFilename === null) @@ -111,7 +114,7 @@ export default async function handler( await connection.execute(`CREATE DATABASE \`${databaseName}\``); await run( [ - `mysql -u${process.env.MYSQL_USERNAME} `, + `mariadb -u${process.env.MYSQL_USERNAME} `, `-p${process.env.MYSQL_PASSWORD} `, `-h${process.env.MYSQL_HOST} `, `--database "${databaseName}" < ${filePath}`, diff --git a/app/pages/api/dockerhub/[image].ts b/app/pages/api/dockerhub/[image].ts index aedd0f5..caa4dbf 100644 --- a/app/pages/api/dockerhub/[image].ts +++ b/app/pages/api/dockerhub/[image].ts @@ -19,6 +19,9 @@ type Response = { readonly name: string; readonly last_updated: string; readonly digest: string; + readonly images: RA<{ + readonly architecture: string; + }>; }>; readonly next: string | undefined; }; @@ -35,7 +38,12 @@ const processTagsResponse = (tags: Response['results']): IR => Object.fromEntries( tags // Latest is an unpredictable branch, thus will exclude it - .filter(({ name }) => !name.startsWith('sha-') && name !== 'latest') + .filter( + ({ name, images }) => + !name.startsWith('sha-') && + name !== 'latest' && + images.some(({ architecture }) => architecture === 'arm64') + ) .map(({ name, last_updated, digest }) => [ name, { diff --git a/app/pages/databases/upload.tsx b/app/pages/databases/upload.tsx index ad60d89..b991824 100644 --- a/app/pages/databases/upload.tsx +++ b/app/pages/databases/upload.tsx @@ -6,6 +6,7 @@ import Layout from '../../components/Layout'; import { useApi } from '../../components/useApi'; import { useDatabases } from '../index'; import { localization } from '../../const/localization'; +import { generateDatabaseNameWithDate } from '../../lib/databaseNameHelper'; const bytesToMb = (size: number): number => Math.round((size / 1024 / 1024) * 100) / 100; @@ -22,7 +23,8 @@ export default function Index(): JSX.Element { const isConflict = typeof databases === 'object' && - databases.some(({ name }) => name === databaseName); + Boolean(databaseName) && + databases.some(({ name }) => name === generateDatabaseNameWithDate(databaseName)); return ( @@ -91,6 +93,16 @@ export default function Index(): JSX.Element { diskUsage.data.free + fileSize >= diskUsage.data.size * 0.99 ? (

{localization.notEnoughSpace}

) : undefined} + {databaseName && ( +
+ +
+ {generateDatabaseNameWithDate(databaseName)} +
+
+ )}