Files
tatort/src/routes/(angemeldet)/upload/+page.svelte

377 lines
11 KiB
Svelte

<script lang="ts">
import { deserialize } from '$app/forms';
import Alert from '$lib/components/Alert.svelte';
import Button from '$lib/components/Button.svelte';
import Modal from '$lib/components/Modal/Modal.svelte';
import ModalTitle from '$lib/components/Modal/ModalTitle.svelte';
import ModalContent from '$lib/components/Modal/ModalContent.svelte';
import ModalFooter from '$lib/components/Modal/ModalFooter.svelte';
import shortenFileSize from '$lib/helper/shortenFileSize.js';
import Exclamation from '$lib/icons/Exclamation.svelte';
import FileRect from '$lib/icons/File-rect.svelte';
export let form;
let open = false;
let inProgress = false;
let vorgang = '';
const code_len = 8;
function generate_token() {
return Math.random()
.toString(36)
.slice(2, 2 + code_len);
}
let zugangscode = ''
let zugangscode_old = ''
$: zugangscode_old = generate_token();
$: zugangscode = zugangscode_old
let case_existing = undefined;
$: case_existing = false;
let name = '';
let etag: string | null = null;
let files: FileList | null = null;
$: inProgress = form === null;
let formErrors: Record<string,any> | null;
async function validateForm() {
let data = new FormData();
data.append('vorgang', vorgang);
data.append('name', name);
data.append('zugangscode', zugangscode);
const response = await fetch('?/validate', { method: 'POST', body: data });
/** @type {import('@sveltejs/kit').ActionResult} */
const result = deserialize(await response.text());
let success = true;
if (result.type === 'success') {
formErrors = null;
} else {
if (result.type === 'failure' && result.data) formErrors = result.data;
success = false;
}
if (!files?.length) {
formErrors = { file: 'Sie haben keine Datei ausgewählt.', ...formErrors };
success = false;
}
if (!(await check_valid_glb_file())) {
formErrors = { file: 'Keine gültige .GLD-Datei', ...formErrors };
success = false;
}
return success;
}
async function getUrl() {
let data = new FormData();
data.append('vorgang', vorgang);
data.append('name', name);
data.append('zugangscode', zugangscode);
if (files?.length === 1) {
data.append('type', files[0].type);
data.append('fileName', files[0].name);
}
const response = await fetch('?/url', { method: 'POST', body: data });
/** @type {import('@sveltejs/kit').ActionResult} */
const result = deserialize(await response.text());
if (result.type === 'success') return result.data?.url;
return null;
}
async function buttonClick(event: MouseEvent) {
if (!(await validateForm())) {
event.preventDefault();
return;
}
const url = await getUrl();
open = true;
inProgress = true;
fetch(url, { method: 'PUT', body: files[0] })
.then((response) => {
inProgress = false;
etag = '123';
})
.catch((err) => {
inProgress = false;
etag = null;
console.log('ERROR', err);
});
}
function uploadSuccessful() {
open = false;
vorgang = '';
name = '';
files = null;
}
// `val` is hex string
function swap_endian(val) {
// from https://www.geeksforgeeks.org/bit-manipulation-swap-endianness-of-a-number/
let leftmost_byte = (val & eval(0x000000ff)) >> 0;
let left_middle_byte = (val & eval(0x0000ff00)) >> 8;
let right_middle_byte = (val & eval(0x00ff0000)) >> 16;
let rightmost_byte = (val & eval(0xff000000)) >> 24;
leftmost_byte <<= 24;
left_middle_byte <<= 16;
right_middle_byte <<= 8;
rightmost_byte <<= 0;
let res = leftmost_byte | left_middle_byte | right_middle_byte | rightmost_byte;
return res;
}
async function check_valid_glb_file() {
// GLD Header, magic value 0x46546C67, identifies data as binary glTF, 4 bytes
// little endian!
const GLD_MAGIC = 0x46546c67;
// big endian!
let file = files[0];
let file_header = file.slice(0, 4);
let header_bytes = await file_header.bytes();
let file_header_hex = '0x' + header_bytes.toHex().toString();
if (GLD_MAGIC == swap_endian(file_header_hex)) {
return true;
} else {
return false;
}
return true;
}
// `/(angemeldet)/view` return true or false
async function case_exists(case_no) {
if (case_no == '') {
zugangscode = zugangscode_old;
}
// ping `/view` with caseNumber in POST body
let url = '/view';
let data = new FormData();
data.append('caseNumber', case_no);
// fetch code in parallel
const code = await get_code(case_no);
if (code != -1) {
zugangscode = code;
case_existing = true;
return true
}
const response = await fetch(url, { method: 'POST', body: data });
const res_json = await response.json();
const status = res_json.status;
if (status != 303) {
case_existing = false;
zugangscode = zugangscode_old;
}
return false;
}
async function get_code(case_no) {
if (case_no == '') return;
let url = `/api/list/${case_no}/code`;
const response = await fetch(url);
if (response.status == 200) {
return response.text();
} else {
return -1;
}
}
</script>
<div class="mx-auto max-w-2xl">
<div class="flex flex-col items-center justify-center w-full">
<h1 class="text-xl">Datei zu Vorgang hinzufügen</h1>
</div>
<div>
<div class="space-y-12">
<div class="border-b border-gray-900/10 pb-12">
<p class="mt-8 text-sm leading-6 text-gray-600">
This information will be displayed publicly so be careful what you share.
</p>
<div class="mt-10 grid grid-cols-1 gap-x-6 gap-y-8">
<div>
<label for="vorgang" class="block text-sm font-medium leading-6 text-gray-900"
><span class="flex"
>{#if formErrors?.vorgang}
<span class="inline-block mr-1"><Exclamation /></span>
{/if} Vorgang</span
></label
>
<div class="mt-2">
<div
class="flex rounded-md shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-inset focus-within:ring-indigo-600"
>
<input
bind:value={vorgang}
type="text"
name="vorgang"
id="vorgang"
autocomplete={vorgang}
class="block flex-1 border-0 bg-transparent py-1.5 pl-1 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6"
on:input={() => case_exists(vorgang)}
/>
</div>
</div>
{#if formErrors?.vorgang}
<p class="block text-sm leading-6 text-red-900 mt-2">{formErrors.vorgang}</p>
{/if}
{#if case_existing && vorgang.length > 0}
<span>Datei wird zum existierenden Vorgang hinzugefügt.</span>
{:else if vorgang.length > 0}
<span>Neuer Vorgang wird angelegt.</span>
{/if}
</div>
<div>
<label for="name" class="block text-sm font-medium leading-6 text-gray-900"
><span class="flex"
>{#if formErrors?.name}
<span class="inline-block mr-1"><Exclamation /></span>
{/if} Name</span
></label
>
<div class="mt-2">
<div
class="flex rounded-md shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-inset focus-within:ring-indigo-600"
>
<input
bind:value={name}
type="text"
name="name"
id="name"
autocomplete={name}
class="block flex-1 border-0 bg-transparent py-1.5 pl-1 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6"
/>
</div>
</div>
{#if formErrors?.name}
<p class="block text-sm leading-6 text-red-900 mt-2">{formErrors.name}</p>
{/if}
</div>
<div>
<label for="zugangscode" class="block text-sm font-medium leading-6 text-gray-900"
><span class="flex"
>{#if formErrors?.zugangscode}
<span class="inline-block mr-1"><Exclamation /></span>
{/if} Zugangscode</span
></label
>
<div class="mt-2">
<div
class="flex rounded-md shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-inset focus-within:ring-indigo-600"
>
<input
bind:value={zugangscode}
type="text"
name="zugangscode"
id="zugangscode"
on:input="{ (ev) => { zugangscode_old = ev.target.value }}"
class="block flex-1 border-0 bg-transparent py-1.5 pl-1 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6"
/>
</div>
<button
class="rounded-md bg-blue-500 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
on:click="{() => {
zugangscode = zugangscode_old = generate_token(); }}"
type="button">
Generiere Zugangscode
</button>
</div>
{#if formErrors?.code}
<p class="block text-sm leading-6 text-red-900 mt-2">{formErrors.code}</p>
{/if}
</div>
<div class="col-span-full">
<label for="file" class="block text-sm font-medium leading-6 text-gray-900"
><span class="flex"
>{#if formErrors?.file}
<span class="inline-block mr-1"><Exclamation /></span>
{/if} Datei</span
></label
>
<div
class="mt-2 flex justify-center rounded-lg border border-dashed border-gray-900/25 px-6 py-10"
>
<div class="text-center">
<FileRect />
<div class="mt-4 flex text-sm leading-6 text-gray-600">
<label
for="file"
class="relative cursor-pointer rounded-md bg-white font-semibold text-indigo-600 focus-within:outline-none focus-within:ring-2 focus-within:ring-indigo-600 focus-within:ring-offset-2 hover:text-indigo-500"
>
<span>Wähle eine Datei aus</span>
<input id="file" bind:files name="file" type="file" class="sr-only" />
</label>
<p class="pl-1">oder ziehe sie ins Feld</p>
</div>
<p class="text-xs leading-5 text-gray-600">GLB Dateien bis zu 1GB</p>
{#if files?.length}
<div class="flex justify-center text-xs">
<p class="mx-2">Datei: <span class="font-bold">{files[0].name}</span></p>
<p class="mx-2">
Größe: <span class="font-bold">{shortenFileSize(files[0].size)}</span>
</p>
</div>
{/if}
</div>
</div>
{#if formErrors?.file}
<p class="block text-sm leading-6 text-red-900 mt-2">{formErrors.file}</p>
{/if}
</div>
</div>
</div>
<div class="mt-6 flex items-center justify-end gap-x-6">
<button type="button" class="text-sm font-semibold leading-6 text-gray-900">Cancel</button>
<Button
on:click={buttonClick}
class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
>Save</Button
>
</div>
</div>
</div>
<Modal {open}
><ModalTitle>Upload</ModalTitle><ModalContent>
{#if inProgress}
<p class="py-2 mb-1">Upload läuft...</p>
{:else if etag}
<Alert class="w-full">Upload erfolgreich</Alert>
{:else}
<Alert class="w-full" type="error">Fehler beim Upload</Alert>
{/if}
</ModalContent>
<ModalFooter><Button disabled={inProgress} on:click={uploadSuccessful}>Ok</Button></ModalFooter>
</Modal>
</div>