This commit is contained in:
11
src/app.d.ts
vendored
Normal file
11
src/app.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
12
src/app.html
Normal file
12
src/app.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
15
src/app.scss
Normal file
15
src/app.scss
Normal file
@ -0,0 +1,15 @@
|
||||
/* Override global Sass variables from the /utilities folder */
|
||||
@use 'bulma/sass/utilities' with (
|
||||
$link: $pink
|
||||
);
|
||||
@use 'bulma/sass/base' with (
|
||||
$body-overflow-y: auto
|
||||
);
|
||||
/* Import the components you need */
|
||||
@use 'bulma/sass/elements';
|
||||
@use 'bulma/sass/form';
|
||||
@use 'bulma/sass/components';
|
||||
@use 'bulma/sass/grid';
|
||||
@use 'bulma/sass/helpers';
|
||||
@use 'bulma/sass/layout';
|
||||
@use 'bulma/sass/themes';
|
8
src/index.test.ts
Normal file
8
src/index.test.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import safePath from '$lib';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('safe path', () => {
|
||||
it('removes non alphanum from string', () => {
|
||||
expect(safePath('../../!=-.,/abc123')).toBe('abc123');
|
||||
});
|
||||
});
|
5
src/lib/index.ts
Normal file
5
src/lib/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
||||
|
||||
const safePath = (input: string) => input.replace(/\W/g, '');
|
||||
|
||||
export default safePath;
|
9
src/routes/+error.svelte
Normal file
9
src/routes/+error.svelte
Normal file
@ -0,0 +1,9 @@
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
</script>
|
||||
|
||||
{#if $page.status === 404}
|
||||
<div class="box">
|
||||
<h3>Seite nicht gefunden. <a href="/">Zurück zur Startseite.</a></h3>
|
||||
</div>
|
||||
{/if}
|
47
src/routes/+layout.svelte
Normal file
47
src/routes/+layout.svelte
Normal file
@ -0,0 +1,47 @@
|
||||
<script>
|
||||
import '../app.scss';
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<section class="section">
|
||||
<div class="container is-max-desktop">
|
||||
<div class="columns">
|
||||
<div class="column is-two-thirds is-offset-one-fifth">
|
||||
<h1 class="title">Gabi und Hannes Fotochallenge</h1>
|
||||
<h2 class="subtitle">Lösungen für die Fotochallenge</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</header>
|
||||
|
||||
<main id="wrapper">
|
||||
<section class="section">
|
||||
<div class="container is-max-desktop">
|
||||
<div id="content">
|
||||
<div class="columns">
|
||||
<div class="column is-half is-offset-one-quarter">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
:global(body) {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#wrapper {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#content {
|
||||
text-align: justify;
|
||||
hyphens: auto;
|
||||
}
|
||||
</style>
|
60
src/routes/+page.server.ts
Normal file
60
src/routes/+page.server.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
||||
import { fail } from '@sveltejs/kit';
|
||||
import type { RequestEvent } from './$types';
|
||||
import safePath from '$lib';
|
||||
import { hash } from 'crypto';
|
||||
import path from 'path';
|
||||
|
||||
const storagePath: string = './uploads';
|
||||
|
||||
const mkdirIfNotExists = (path: string) => {
|
||||
if (!existsSync(path)) {
|
||||
mkdirSync(path);
|
||||
}
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
default: async ({ request }: RequestEvent) => {
|
||||
const data = await request.formData();
|
||||
|
||||
const formFiles = data.getAll('files');
|
||||
if (!formFiles) {
|
||||
return fail(400, { field: 'files', files: formFiles, missing: true });
|
||||
} else if (!(formFiles as File[])) {
|
||||
return fail(400, { field: 'files', files: formFiles, incorrect: true });
|
||||
}
|
||||
const files = formFiles as File[];
|
||||
console.log(files);
|
||||
if (files.length === 0) {
|
||||
return fail(400, { field: 'files', files: formFiles, empty: true });
|
||||
}
|
||||
|
||||
const formName = data.get('name');
|
||||
if (!formName) {
|
||||
return fail(400, { field: 'name', name: formName, missing: true });
|
||||
} else if (!(formName as string)) {
|
||||
return fail(400, { field: 'name', name: formName, incorrect: true });
|
||||
}
|
||||
|
||||
const name = safePath(formName as string);
|
||||
|
||||
files.forEach(async (file) => {
|
||||
const outPath = `${storagePath}/${name}`;
|
||||
const content = Buffer.from(await file.arrayBuffer());
|
||||
const ext = path.extname(file.name);
|
||||
|
||||
mkdirIfNotExists(outPath);
|
||||
const filename = hash('sha1', content);
|
||||
const fullPath = `${outPath}/${filename}${ext}`;
|
||||
if (existsSync(fullPath)) {
|
||||
console.warn(`${fullPath} has already been uploaded. Skipping...`);
|
||||
} else {
|
||||
writeFileSync(fullPath, Buffer.from(await file.arrayBuffer()), { flag: 'a+' });
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: true
|
||||
};
|
||||
}
|
||||
};
|
94
src/routes/+page.svelte
Normal file
94
src/routes/+page.svelte
Normal file
@ -0,0 +1,94 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import type { ActionData } from './$types';
|
||||
|
||||
export let form: ActionData;
|
||||
|
||||
let files: FileList;
|
||||
let sending = false;
|
||||
|
||||
const siPrefixes = new Map([
|
||||
[1_000_000, 'M'],
|
||||
[1_000, 'k']
|
||||
]);
|
||||
const fileSize = (files: FileList) => {
|
||||
const size = Array.from(files)
|
||||
.map((f) => f.size)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
return (
|
||||
Array.from(siPrefixes)
|
||||
.filter(([k]) => size >= k)
|
||||
.map(([k, v]) => `${(size / k).toFixed(1)} ${v}B`)[0] ?? `${size} bytes`
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
<form
|
||||
enctype="multipart/form-data"
|
||||
class="box"
|
||||
method="POST"
|
||||
use:enhance={() => {
|
||||
sending = true;
|
||||
return ({ update }) => {
|
||||
update({ invalidateAll: true }).finally(async () => {
|
||||
sending = false;
|
||||
});
|
||||
};
|
||||
}}
|
||||
>
|
||||
{#if sending}
|
||||
<div class="notification is-info">Wird hochgeladen...</div>
|
||||
{:else if form?.success}
|
||||
<div class="notification is-success">Erfolgreich hochgeladen</div>
|
||||
{/if}
|
||||
<div class="field">
|
||||
<label for="name" class="label">Name</label>
|
||||
<div class="control">
|
||||
<input
|
||||
id="name"
|
||||
class="input"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Name"
|
||||
value={form?.name ?? ''}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{#if form?.field === 'name'}
|
||||
{#if form?.missing}
|
||||
<p class="help is-danger">Bitte einen Namen angeben</p>
|
||||
{:else if form?.incorrect}
|
||||
<p class="help is-danger">Ungültiger Name</p>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="file is-centered has-name is-boxed">
|
||||
<label class="file-label">
|
||||
<input class="file-input" type="file" name="files" bind:files multiple required />
|
||||
<span class="file-cta">
|
||||
<span class="file-label">Fotos auswählen...</span>
|
||||
</span>
|
||||
{#if files}
|
||||
<span class="file-name"
|
||||
>{files.length} Bild{#if files.length > 1}er{/if} ausgewählt ({fileSize(files)})</span
|
||||
>
|
||||
{:else}
|
||||
<span class="file-name">Keine Bilder ausgewählt</span>
|
||||
{/if}
|
||||
{#if form?.field === 'files'}
|
||||
{#if form?.missing || form?.empty}
|
||||
<p class="help is-danger">Bitte mindestens eine Datei auswählen</p>
|
||||
{:else if form?.incorrect}
|
||||
<p class="help is-danger">Ungültige Dateien</p>
|
||||
{/if}
|
||||
{/if}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="field is-grouped is-grouped-centered">
|
||||
<div class="control">
|
||||
<button class="button is-link">Hochladen</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
2
src/variables.scss
Normal file
2
src/variables.scss
Normal file
@ -0,0 +1,2 @@
|
||||
/* Set your brand colors */
|
||||
$pink: pink;
|
Reference in New Issue
Block a user