(add): Backup/Restore
This commit is contained in:
@@ -7,8 +7,8 @@ from fastapi import APIRouter, Header
|
|||||||
class Basket:
|
class Basket:
|
||||||
prefix: str
|
prefix: str
|
||||||
b_id: int
|
b_id: int
|
||||||
description: str = ""
|
description: str | None = ""
|
||||||
donors: str = ""
|
donors: str | None = ""
|
||||||
winning_ticket: int = 0
|
winning_ticket: int = 0
|
||||||
|
|
||||||
class BasketRepo(RepoTemplate):
|
class BasketRepo(RepoTemplate):
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from system.auth import auth_router
|
from system.auth import auth_router
|
||||||
|
from system.backuprestore import backuprestore_router
|
||||||
from system.prefixes import prefix_router
|
from system.prefixes import prefix_router
|
||||||
from data.tickets import tickets_router
|
from data.tickets import tickets_router
|
||||||
from data.baskets import basket_router
|
from data.baskets import basket_router
|
||||||
@@ -15,3 +16,4 @@ def imp_routers(app: FastAPI):
|
|||||||
app.include_router(drawing_router)
|
app.include_router(drawing_router)
|
||||||
app.include_router(reports_router)
|
app.include_router(reports_router)
|
||||||
app.include_router(search_router)
|
app.include_router(search_router)
|
||||||
|
app.include_router(backuprestore_router)
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
from data.baskets import Basket, BasketRepo
|
||||||
|
from data.tickets import Ticket, TicketRepo
|
||||||
|
from .prefixes import Prefix, PrefixRepo
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import List
|
||||||
|
from fastapi import APIRouter, Header
|
||||||
|
from .auth import AuthRepo
|
||||||
|
|
||||||
|
chunk_size = 300
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BackupFile:
|
||||||
|
prefixes: List[Prefix] = field(default_factory=list)
|
||||||
|
baskets: List[Basket] = field(default_factory=list)
|
||||||
|
tickets: List[Ticket] = field(default_factory=list)
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ReturnMessage:
|
||||||
|
message: str
|
||||||
|
|
||||||
|
backuprestore_router = APIRouter(prefix="/api/backuprestore")
|
||||||
|
|
||||||
|
@backuprestore_router.get("")
|
||||||
|
def get_backup_file(tam_key: str = Header("")):
|
||||||
|
AuthRepo().verify_key(tam_key)
|
||||||
|
return BackupFile(prefixes=PrefixRepo().get_all_prefixes(), baskets=BasketRepo().get_all_baskets(), tickets=TicketRepo().get_all_tickets())
|
||||||
|
|
||||||
|
@backuprestore_router.post("")
|
||||||
|
def post_backup_file(bf: BackupFile, tam_key: str = Header("")):
|
||||||
|
AuthRepo().verify_key(tam_key)
|
||||||
|
for i in range(0, len(bf.prefixes), chunk_size):
|
||||||
|
PrefixRepo().post_prefixes(bf.prefixes[i:i+chunk_size])
|
||||||
|
for i in range(0, len(bf.baskets), chunk_size):
|
||||||
|
BasketRepo().post_baskets(bf.baskets[i:i+chunk_size])
|
||||||
|
for i in range(0, len(bf.tickets), chunk_size):
|
||||||
|
TicketRepo().post_tickets(bf.tickets[i:i+chunk_size])
|
||||||
|
return ReturnMessage(message="Backup file imported successfully.")
|
||||||
@@ -36,9 +36,13 @@
|
|||||||
type="number"
|
type="number"
|
||||||
id="id_from"
|
id="id_from"
|
||||||
class={iS.normal}
|
class={iS.normal}
|
||||||
|
title="Alt + Q"
|
||||||
onclick={(e) => {
|
onclick={(e) => {
|
||||||
e.target.select();
|
e.target.select();
|
||||||
}}
|
}}
|
||||||
|
onchange={() => {
|
||||||
|
if (functions.pagerFromUpdate) functions.pagerFromUpdate();
|
||||||
|
}}
|
||||||
bind:value={pager.idFrom}
|
bind:value={pager.idFrom}
|
||||||
/>
|
/>
|
||||||
<div>-</div>
|
<div>-</div>
|
||||||
@@ -46,6 +50,7 @@
|
|||||||
type="number"
|
type="number"
|
||||||
id="id_to"
|
id="id_to"
|
||||||
class={iS.normal}
|
class={iS.normal}
|
||||||
|
title="Alt + W"
|
||||||
onclick={(e) => {
|
onclick={(e) => {
|
||||||
e.target.select();
|
e.target.select();
|
||||||
}}
|
}}
|
||||||
@@ -62,6 +67,7 @@
|
|||||||
{#if functions.prevPage}
|
{#if functions.prevPage}
|
||||||
<button
|
<button
|
||||||
class={bS[prefix.color]}
|
class={bS[prefix.color]}
|
||||||
|
title="Alt + B"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
functions.prevPage();
|
functions.prevPage();
|
||||||
}}>Prev Page</button
|
}}>Prev Page</button
|
||||||
@@ -70,6 +76,7 @@
|
|||||||
{#if functions.nextPage}
|
{#if functions.nextPage}
|
||||||
<button
|
<button
|
||||||
class={bS[prefix.color]}
|
class={bS[prefix.color]}
|
||||||
|
title="Alt + N"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
functions.nextPage();
|
functions.nextPage();
|
||||||
}}>Next Page</button
|
}}>Next Page</button
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export const iS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const rBS = {
|
export const rBS = {
|
||||||
white: 'even:bg-gray-100',
|
white: 'even:bg-gray-200',
|
||||||
blue: 'even:bg-blue-100',
|
blue: 'even:bg-blue-100',
|
||||||
yellow: 'even:bg-yellow-100',
|
yellow: 'even:bg-yellow-100',
|
||||||
green: 'even:bg-green-100',
|
green: 'even:bg-green-100',
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { db } from '$lib/server/db';
|
||||||
|
import { baskets, prefixes, tickets } from '$lib/server/db/schema';
|
||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
import { sql } from 'drizzle-orm';
|
||||||
|
|
||||||
|
const chunk_size = 300;
|
||||||
|
|
||||||
|
export const GET = async () => {
|
||||||
|
const data = {};
|
||||||
|
data.prefixes = await db.select().from(prefixes);
|
||||||
|
data.baskets = await db.select().from(baskets);
|
||||||
|
data.tickets = await db.select().from(tickets);
|
||||||
|
return json(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const POST = async ({ request }) => {
|
||||||
|
const reqData = await request.json();
|
||||||
|
for (let i = 0; i < reqData.prefixes.length; i += chunk_size) {
|
||||||
|
const chunk = reqData.prefixes.slice(i, i + chunk_size);
|
||||||
|
await db
|
||||||
|
.insert(prefixes)
|
||||||
|
.values(chunk)
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: prefixes.prefix,
|
||||||
|
set: { color: sql`EXCLUDED.color`, weight: sql`EXCLUDED.weight` }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (let i = 0; i < reqData.baskets.length; i += chunk_size) {
|
||||||
|
const chunk = reqData.baskets.slice(i, i + chunk_size);
|
||||||
|
await db
|
||||||
|
.insert(baskets)
|
||||||
|
.values(chunk)
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: [baskets.prefix, baskets.b_id],
|
||||||
|
set: {
|
||||||
|
description: sql`EXCLUDED.description`,
|
||||||
|
donors: sql`EXCLUDED.donors`,
|
||||||
|
winning_ticket: sql`EXCLUDED.winning_ticket`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (let i = 0; i < reqData.tickets.length; i += chunk_size) {
|
||||||
|
const chunk = reqData.tickets.slice(i, i + chunk_size);
|
||||||
|
await db
|
||||||
|
.insert(tickets)
|
||||||
|
.values(chunk)
|
||||||
|
.onConflictDoNothing({
|
||||||
|
target: [tickets.prefix, tickets.t_id],
|
||||||
|
set: {
|
||||||
|
first_name: sql`EXCLUDED.first_name`,
|
||||||
|
last_name: sql`EXCLUDED.last_name`,
|
||||||
|
phone_number: sql`EXCLUDED.phone_number`,
|
||||||
|
pref: sql`EXCLUDED.pref`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return json({ details: 'Data loaded successfully.' });
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { db } from '$lib/server/db/index.js';
|
||||||
|
import { baskets, prefixes, tickets } from '$lib/server/db/schema';
|
||||||
|
import { getPath, getSettings } from '$lib/server/settings/index.js';
|
||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export const HEAD = async ({ params }) => {
|
||||||
|
const target = params.target;
|
||||||
|
const pushData = {};
|
||||||
|
if (target === 'prefixes') {
|
||||||
|
pushData.prefixes = await db.select().from(prefixes);
|
||||||
|
} else if (target === 'tickets') {
|
||||||
|
pushData.tickets = await db.select().from(tickets);
|
||||||
|
} else if (target === 'baskets') {
|
||||||
|
pushData.baskets = await db.select().from(baskets);
|
||||||
|
} else {
|
||||||
|
throw error(400, 'Can only push prefixes, tickets, or baskets.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const s = getSettings();
|
||||||
|
if (s.remote_server) {
|
||||||
|
const connStr = getPath(s);
|
||||||
|
const res = await fetch(`${connStr}/api/backuprestore`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'TAM-KEY': s.remote_key, 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(pushData)
|
||||||
|
});
|
||||||
|
if (!res.ok) throw error(res.status, res.statusText);
|
||||||
|
return new Response(null, { status: 200, statusText: 'Data pushed successfully.' });
|
||||||
|
} else {
|
||||||
|
throw error(500, 'Server not set.');
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { getPath, getSettings } from '$lib/server/settings';
|
||||||
|
import { error, json } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export const GET = async () => {
|
||||||
|
const s = getSettings();
|
||||||
|
if (s.remote_server) {
|
||||||
|
const connStr = getPath(s);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${connStr}/api/backuprestore`, {
|
||||||
|
headers: { TAM_KEY: s.remote_key }
|
||||||
|
});
|
||||||
|
if (!res.ok) throw error(res.status);
|
||||||
|
const data = await res.json();
|
||||||
|
return json(data);
|
||||||
|
} catch {
|
||||||
|
return json({});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return json({});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const POST = async ({ request }) => {
|
||||||
|
const resData = await request.json();
|
||||||
|
const s = getSettings();
|
||||||
|
if (s.remote_server) {
|
||||||
|
const connStr = getPath(s);
|
||||||
|
const res = await fetch(`${connStr}/api/backuprestore`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'TAM-KEY': s.remote_key, 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(resData)
|
||||||
|
});
|
||||||
|
if (!res.ok) throw error(res.status);
|
||||||
|
return json([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
curIdx = idx;
|
curIdx = idx;
|
||||||
const elemIdx = document.getElementById(`${idx}_first`);
|
const elemIdx = document.getElementById(`${idx}_first`);
|
||||||
if (elemIdx) {
|
if (elemIdx) {
|
||||||
elemIdx.focus();
|
elemIdx.select();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -44,6 +44,13 @@
|
|||||||
items = [...resData];
|
items = [...resData];
|
||||||
setTimeout(() => focusIdx(0));
|
setTimeout(() => focusIdx(0));
|
||||||
},
|
},
|
||||||
|
pagerFromUpdate(){
|
||||||
|
if (itemsLength === 0) {
|
||||||
|
pager.idTo = pager.idFrom;
|
||||||
|
} else {
|
||||||
|
pager.idTo = pager.idFrom + (itemsLength - 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
save: async () => {
|
save: async () => {
|
||||||
if (itemsBuffer.length > 0) {
|
if (itemsBuffer.length > 0) {
|
||||||
const res = await fetch('/api/baskets', {
|
const res = await fetch('/api/baskets', {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
curIdx = idx;
|
curIdx = idx;
|
||||||
const elemIdx = document.getElementById(`${idx}_first`);
|
const elemIdx = document.getElementById(`${idx}_first`);
|
||||||
if (elemIdx) {
|
if (elemIdx) {
|
||||||
elemIdx.focus();
|
elemIdx.select();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -44,6 +44,13 @@
|
|||||||
items = [...resData];
|
items = [...resData];
|
||||||
setTimeout(() => focusIdx(0));
|
setTimeout(() => focusIdx(0));
|
||||||
},
|
},
|
||||||
|
pagerFromUpdate(){
|
||||||
|
if (itemsLength === 0) {
|
||||||
|
pager.idTo = pager.idFrom;
|
||||||
|
} else {
|
||||||
|
pager.idTo = pager.idFrom + (itemsLength - 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
save: async () => {
|
save: async () => {
|
||||||
if (itemsBuffer.length > 0) {
|
if (itemsBuffer.length > 0) {
|
||||||
const res = await fetch('/api/drawing', {
|
const res = await fetch('/api/drawing', {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
<a href={resolve('/settings/auth-keys')} class={bS.gray}>Auth Keys</a>
|
<a href={resolve('/settings/auth-keys')} class={bS.gray}>Auth Keys</a>
|
||||||
{/if}
|
{/if}
|
||||||
<a href={resolve('/settings/prefixes')} class={bS.gray}>Prefixes</a>
|
<a href={resolve('/settings/prefixes')} class={bS.gray}>Prefixes</a>
|
||||||
|
<a href={resolve('/settings/backuprestore')} class={bS.gray}>Backup/Restore</a>
|
||||||
</HeaderBar>
|
</HeaderBar>
|
||||||
<h1 class="text-xl font-bold">{pageTitle}</h1>
|
<h1 class="text-xl font-bold">{pageTitle}</h1>
|
||||||
<div class="flex flex-col gap-1 w-full py-1">
|
<div class="flex flex-col gap-1 w-full py-1">
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { getSettings } from "$lib/server/settings/index.js"
|
||||||
|
|
||||||
|
export const load = async () => {
|
||||||
|
const s = getSettings();
|
||||||
|
return { remoteServer: s.remote_server };
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
<script>
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
|
import HeaderBar from '$lib/client/components/HeaderBar.svelte';
|
||||||
|
import { bS, iS } from '$lib/client/styles';
|
||||||
|
|
||||||
|
const pageTitle = 'Backup and Restore | TAM';
|
||||||
|
|
||||||
|
let { data } = $props();
|
||||||
|
let remoteServer = $derived(data.remoteServer);
|
||||||
|
let uploadFile = $state();
|
||||||
|
let contents = $state('');
|
||||||
|
let results = $state('');
|
||||||
|
|
||||||
|
function setResult(newResult) {
|
||||||
|
results = newResult;
|
||||||
|
setTimeout(() => (results = ''), 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadBackupFile(target) {
|
||||||
|
let fetch_url;
|
||||||
|
if (target === 'local') {
|
||||||
|
fetch_url = '/api/backuprestore/local';
|
||||||
|
} else if (target === 'remote') {
|
||||||
|
fetch_url = '/api/backuprestore/remote';
|
||||||
|
}
|
||||||
|
const now = new Date();
|
||||||
|
const date = {
|
||||||
|
year: now.getFullYear(),
|
||||||
|
month: String(now.getMonth() + 1).padStart(2, '0'),
|
||||||
|
day: String(now.getDate()).padStart(2, '0'),
|
||||||
|
hour: String(now.getHours()).padStart(2, '0'),
|
||||||
|
minutes: String(now.getMinutes()).padStart(2, '0')
|
||||||
|
};
|
||||||
|
const res = await fetch(fetch_url);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
const jsonString = JSON.stringify(data, null, 2);
|
||||||
|
const blob = new Blob([jsonString], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `TAM_${date.year}-${date.month}-${date.day} ${date.hour}:${date.minutes}.json`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fileUpload(target) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsText(uploadFile[0], 'utf-8');
|
||||||
|
reader.onload = function (e) {
|
||||||
|
contents = String(e.target.result);
|
||||||
|
};
|
||||||
|
console.log(contents);
|
||||||
|
setTimeout(async () => {
|
||||||
|
const res = await fetch(`/api/backuprestore/${target}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: contents
|
||||||
|
});
|
||||||
|
if (res.ok) setResult('File uploaded successfully. Check to see if your data exists.');
|
||||||
|
else setResult('Error uploading file.');
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pushData(target) {
|
||||||
|
const res = await fetch(`/api/backuprestore/push/${target}`, { method: 'HEAD' });
|
||||||
|
const targetStr = target.charAt(0).toUpperCase() + target.slice(1);
|
||||||
|
if (res.ok) {
|
||||||
|
setResult(`${targetStr} pushed successfully.`);
|
||||||
|
} else {
|
||||||
|
setResult(`Error pushing ${targetStr}. Error [${res.status}] ${res.statusText}.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{pageTitle}</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div id="app_container" class="p-1">
|
||||||
|
<HeaderBar>
|
||||||
|
<a href={resolve('/settings')} class={bS.gray}>Back to Settings</a>
|
||||||
|
<div>Other Settings</div>
|
||||||
|
<a href={resolve('/settings/prefixes')} class={bS.gray}>Prefixes</a>
|
||||||
|
{#if remoteServer}
|
||||||
|
<a href={resolve('/settings/auth-keys')} class={bS.gray}>Auth Keys</a>
|
||||||
|
{/if}
|
||||||
|
</HeaderBar>
|
||||||
|
<h1 class="text-xl font-bold">{pageTitle}</h1>
|
||||||
|
<div class="my-1 p-1 border border-black rounded">
|
||||||
|
<h2 class="text-lg font-bold">Backup File Downloads</h2>
|
||||||
|
<div class="flex flex-row gap-1">
|
||||||
|
<button class={bS.gray} onclick={downloadBackupFile('local')}>Local Data</button>
|
||||||
|
{#if remoteServer}
|
||||||
|
<button class={bS.gray} onclick={downloadBackupFile('remote')}>Remote Data</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="my-1 p-1 border border-black rounded">
|
||||||
|
<h2 class="text-lg font-bold">Backup File Upload</h2>
|
||||||
|
<div class="flex flex-row gap-1 items-center">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".json"
|
||||||
|
class="{iS.normal} rounded file:bg-gray-300 file:border file:border-black file:px-2 file:py-1 file:rounded"
|
||||||
|
bind:files={uploadFile}
|
||||||
|
/>
|
||||||
|
<button class={bS.gray} onclick={() => fileUpload('local')}>Upload to Local</button>
|
||||||
|
{#if remoteServer}
|
||||||
|
<button class={bS.gray} onclick={() => fileUpload('remote')}>Upload to Remote</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if remoteServer}
|
||||||
|
<div class="my-1 p-1 border border-black rounded">
|
||||||
|
<h2 class="text-lg font-bold">Push Data to Server</h2>
|
||||||
|
<div class="flex flex-row gap-1">
|
||||||
|
<button class={bS.gray} onclick={() => pushData('prefixes')}>Push Prefixes</button>
|
||||||
|
<button class={bS.gray} onclick={() => pushData('tickets')}>Push Tickets</button>
|
||||||
|
<button class={bS.gray} onclick={() => pushData('baskets')}>Push Baskets</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="my-1 p-1 border border-black rounded">
|
||||||
|
<span class="font-bold">Status: </span><span>{results}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -44,6 +44,13 @@
|
|||||||
items = [...resData];
|
items = [...resData];
|
||||||
setTimeout(() => focusIdx(0));
|
setTimeout(() => focusIdx(0));
|
||||||
},
|
},
|
||||||
|
pagerFromUpdate(){
|
||||||
|
if (itemsLength === 0) {
|
||||||
|
pager.idTo = pager.idFrom;
|
||||||
|
} else {
|
||||||
|
pager.idTo = pager.idFrom + (itemsLength - 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
save: async () => {
|
save: async () => {
|
||||||
if (itemsBuffer.length > 0) {
|
if (itemsBuffer.length > 0) {
|
||||||
const res = await fetch('/api/tickets', {
|
const res = await fetch('/api/tickets', {
|
||||||
|
|||||||
Reference in New Issue
Block a user