diff --git a/.gitignore b/.gitignore index 454a81b..b9d927a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ node_modules/ .svelte-kit/ build/ data/ +backups/ *.log .env .env.* diff --git a/Dockerfile b/Dockerfile index 8371d66..fdb4bf4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM node:20-bookworm-slim # Tools needed to compile better-sqlite3 RUN apt-get update \ - && apt-get install -y --no-install-recommends python3 make g++ ca-certificates \ + && apt-get install -y --no-install-recommends python3 make g++ ca-certificates tzdata \ && rm -rf /var/lib/apt/lists/* # Run as the node user (uid 1000) — already exists in node images diff --git a/docker-compose.yml b/docker-compose.yml index 88410ec..d12f294 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,4 +15,5 @@ services: - ./data:/app/data environment: - NODE_ENV=development + - TZ=Asia/Dushanbe command: npm run dev diff --git a/src/hooks.server.js b/src/hooks.server.js index b3a79c8..7c70148 100644 --- a/src/hooks.server.js +++ b/src/hooks.server.js @@ -1,8 +1,10 @@ import { getDb } from '$lib/server/db.js'; +import { startBackupScheduler } from '$lib/server/backup.js'; // Open (and warm) the database on server startup so the first request // doesn't pay the cost. getDb(); +startBackupScheduler(); /** @type {import('@sveltejs/kit').Handle} */ export async function handle({ event, resolve }) { diff --git a/src/lib/components/Header.svelte b/src/lib/components/Header.svelte index 9caf0e2..69f1834 100644 --- a/src/lib/components/Header.svelte +++ b/src/lib/components/Header.svelte @@ -24,6 +24,7 @@ {$t('nav.parts')} {$t('nav.movements')} {$t('nav.suppliers')} + {$t('nav.admin')} + +

{$t('admin.auto_note')}

+ + +

{$t('admin.list_title')}

+ +{#if backups.length === 0} +

{$t('admin.no_backups')}

+{:else} + + + + + + + + + + {#each backups as b} + + + + + + {/each} + +
{$t('admin.created_at')}{$t('admin.size')}
{formatWhen(b.createdAt, $locale)}{formatSize(b.size)} + + {$t('admin.download')} + +
async ({ update }) => { await update(); await invalidateAll(); }} + on:submit={(e) => { if (!confirm($t('admin.restore_confirm'))) e.preventDefault(); }}> + + +
+
+

{$t('admin.prune_note')}

+{/if} + + diff --git a/src/routes/admin/download/[name]/+server.js b/src/routes/admin/download/[name]/+server.js new file mode 100644 index 0000000..f2f7eb0 --- /dev/null +++ b/src/routes/admin/download/[name]/+server.js @@ -0,0 +1,24 @@ +import { error } from '@sveltejs/kit'; +import { existsSync, readFileSync, statSync } from 'node:fs'; +import { join } from 'node:path'; +import { BACKUP_DIR, safeName } from '$lib/server/backup.js'; + +export function GET({ params }) { + const { name } = params; + if (!safeName(name)) throw error(400, 'Invalid backup name'); + + const path = join(BACKUP_DIR, name); + if (!existsSync(path)) throw error(404, 'Backup not found'); + + const buf = readFileSync(path); + const size = statSync(path).size; + + return new Response(buf, { + headers: { + 'Content-Type': 'application/octet-stream', + 'Content-Length': String(size), + 'Content-Disposition': `attachment; filename="${name}"`, + 'Cache-Control': 'no-store' + } + }); +}