Compare commits
No commits in common. "main" and "v0.0.1" have entirely different histories.
@ -1,24 +0,0 @@
|
||||
on:
|
||||
workflow_call:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
jobs:
|
||||
shellcheck:
|
||||
name: Misc Linters
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: https://gitea.com/actions/checkout@v4
|
||||
|
||||
- name: Lint shellscripts
|
||||
uses: ludeeus/action-shellcheck@master
|
||||
|
||||
- name: Lint Dockerfile
|
||||
uses: hadolint/hadolint-action@v3.1.0
|
||||
with:
|
||||
dockerfile: ./Containerfile
|
@ -6,9 +6,6 @@ on:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
lints:
|
||||
uses: ./.gitea/workflows/lints.yml
|
||||
|
||||
tests:
|
||||
uses: ./.gitea/workflows/node.yml
|
||||
|
||||
@ -22,10 +19,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Login to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: https://git.vbrandl.net
|
||||
username: ${{ secrets.REGISTRY_USER }}
|
||||
@ -38,7 +35,7 @@ jobs:
|
||||
echo REPO_VERSION=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ./Containerfile
|
||||
|
7
.gitignore
vendored
7
.gitignore
vendored
@ -20,12 +20,5 @@ Thumbs.db
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
# Playwright
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
/playwright/.auth
|
||||
|
||||
uploads
|
||||
.direnv
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM node:22-alpine AS builder
|
||||
FROM node:21-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
@ -7,12 +7,9 @@ RUN npm ci
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM node:22-alpine
|
||||
WORKDIR /app
|
||||
RUN chown -R node:node /app
|
||||
FROM node:21-alpine
|
||||
USER node:node
|
||||
COPY ./container/entrypoint.sh /entrypoint.sh
|
||||
WORKDIR /app
|
||||
COPY package.json .
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/build ./build
|
||||
ENTRYPOINT ["sh", "/entrypoint.sh"]
|
||||
CMD ["node", "./build/index.js"]
|
||||
|
@ -1,12 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
set -e
|
||||
|
||||
export STORAGE_PATH="${STORAGE_PATH:-./uploads}"
|
||||
|
||||
entrypoint() {
|
||||
mkdir -p "${STORAGE_PATH}"
|
||||
node ./build/index.js
|
||||
}
|
||||
|
||||
entrypoint
|
@ -15,19 +15,19 @@ export default [
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
...globals.node
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['**/*.svelte'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: ts.parser,
|
||||
},
|
||||
},
|
||||
parser: ts.parser
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
ignores: ['build/', '.svelte-kit/', 'dist/'],
|
||||
},
|
||||
ignores: ['build/', '.svelte-kit/', 'dist/']
|
||||
}
|
||||
];
|
||||
|
6
flake.lock
generated
6
flake.lock
generated
@ -20,11 +20,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1723637854,
|
||||
"narHash": "sha256-med8+5DSWa2UnOqtdICndjDAEjxr5D7zaIiK4pn0Q7c=",
|
||||
"lastModified": 1720957393,
|
||||
"narHash": "sha256-oedh2RwpjEa+TNxhg5Je9Ch6d3W1NKi7DbRO1ziHemA=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "c3aa7b8938b17aebd2deecf7be0636000d62a2b9",
|
||||
"rev": "693bc46d169f5af9c992095736e82c3488bf7dbb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -15,7 +15,7 @@
|
||||
};
|
||||
in {
|
||||
devShell = pkgs.mkShell {
|
||||
nativeBuildInputs = with pkgs; [ nodejs_latest ];
|
||||
nativeBuildInputs = with pkgs; [ nodejs ];
|
||||
};
|
||||
}
|
||||
);
|
||||
|
2590
package-lock.json
generated
2590
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
26
package.json
26
package.json
@ -1,52 +1,42 @@
|
||||
{
|
||||
"name": "fotochallenge",
|
||||
"version": "0.0.8",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "npm run test:integration && npm run test:unit",
|
||||
"test": "vitest",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write .",
|
||||
"test:integration": "playwright test",
|
||||
"test:unit": "vitest"
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.46.1",
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/adapter-node": "^5.2.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"@types/bunyan": "^1.8.11",
|
||||
"@types/eslint": "^9.0.0",
|
||||
"@types/eslint": "^8.56.7",
|
||||
"@types/node": "^20.14.11",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"bulma": "^1.0.1",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.36.0",
|
||||
"globals": "^15.0.0",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-svelte": "^3.1.2",
|
||||
"sass": "^1.77.5",
|
||||
"simple-svelte-autocomplete": "^2.5.2",
|
||||
"svelte": "^4.2.7",
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-check": "^3.6.0",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.0.0",
|
||||
"typescript-eslint": "^8.0.0-alpha.20",
|
||||
"vite": "^5.0.3",
|
||||
"vitest": "^2.0.0"
|
||||
"vitest": "^2.0.0",
|
||||
"bulma": "^1.0.1",
|
||||
"sass": "^1.77.5"
|
||||
},
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">20"
|
||||
},
|
||||
"dependencies": {
|
||||
"bunyan": "^1.8.15",
|
||||
"uuid": "^10.0.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +0,0 @@
|
||||
import type { PlaywrightTestConfig } from '@playwright/test';
|
||||
|
||||
const config: PlaywrightTestConfig = {
|
||||
webServer: {
|
||||
command: 'npm run build && npm run preview',
|
||||
port: 4173,
|
||||
},
|
||||
testDir: 'tests',
|
||||
testMatch: /(.+\.)?(test|spec)\.[jt]s/,
|
||||
};
|
||||
|
||||
export default config;
|
@ -1,12 +1,4 @@
|
||||
{
|
||||
$schema: 'https://docs.renovatebot.com/renovate-schema.json',
|
||||
extends: ['local>renovate-bot/renovate-config'],
|
||||
packageRules: [
|
||||
{
|
||||
groupName: 'all non-major dependencies',
|
||||
groupSlug: 'all-minor-patch',
|
||||
matchPackageNames: ['*'],
|
||||
matchUpdateTypes: ['minor', 'patch'],
|
||||
},
|
||||
],
|
||||
extends: ['local>renovate-bot/renovate-config']
|
||||
}
|
||||
|
6
src/app.d.ts
vendored
6
src/app.d.ts
vendored
@ -9,9 +9,3 @@ declare global {
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
declare namespace App {
|
||||
interface Locals {
|
||||
requestId: string;
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Gabi und Hannes Fotochallenge</title>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
|
@ -1,43 +0,0 @@
|
||||
import { log, timedExecution, requestIdHeader } from '$lib';
|
||||
import { validate, v7 as uuidv7 } from 'uuid';
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
// use incoming requestId, if it is a valid uuid, else generate one
|
||||
const reqIdFromRequest = event.request.headers.get(requestIdHeader);
|
||||
const { requestId, fromRequest } =
|
||||
reqIdFromRequest && validate(reqIdFromRequest)
|
||||
? { requestId: reqIdFromRequest, fromRequest: true }
|
||||
: { requestId: uuidv7(), fromRequest: false };
|
||||
|
||||
const context = {
|
||||
requestId,
|
||||
route: event.route.id,
|
||||
method: event.request.method,
|
||||
userAgent: event.request.headers.get('user-agent'),
|
||||
clientIP: event.getClientAddress(),
|
||||
};
|
||||
if (fromRequest) {
|
||||
log.trace(context, 'using incoming request-id');
|
||||
}
|
||||
|
||||
// make requestId available to handlers
|
||||
event.locals.requestId = requestId;
|
||||
|
||||
const { executionTime, result: response } = await timedExecution(
|
||||
async () => await resolve(event)
|
||||
);
|
||||
response.headers.set(requestIdHeader, requestId);
|
||||
|
||||
log.info(
|
||||
{
|
||||
executionTime: `${executionTime}ms`,
|
||||
status: response.status,
|
||||
size: response.headers.get('content-length'),
|
||||
...context,
|
||||
},
|
||||
'finished request'
|
||||
);
|
||||
|
||||
return response;
|
||||
};
|
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');
|
||||
});
|
||||
});
|
@ -1,50 +0,0 @@
|
||||
import safePath, { timedExecution } from '$lib';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('safe path', () => {
|
||||
it('reject names with ../', () => {
|
||||
expect(safePath('./uplodas', '../foobar')).toBe(false);
|
||||
});
|
||||
|
||||
it('accept names with ./', () => {
|
||||
expect(safePath('./uplodas', './foobar')).toBe(true);
|
||||
});
|
||||
|
||||
it('reject names with /', () => {
|
||||
expect(safePath('./uplodas', 'foo/bar')).toBe(false);
|
||||
});
|
||||
|
||||
it('accept happy path', () => {
|
||||
expect(safePath('./uplodas', 'foobar')).toBe(true);
|
||||
});
|
||||
|
||||
it('accept names starting with `..`', () => {
|
||||
expect(safePath('./uplodas', '..foobar')).toBe(true);
|
||||
});
|
||||
|
||||
it('accept names ending with `..`', () => {
|
||||
expect(safePath('./uplodas', 'foobar..')).toBe(true);
|
||||
});
|
||||
|
||||
it('accept names starting and ending with `..`', () => {
|
||||
expect(safePath('./uplodas', '..foobar..')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('timedExecution', () => {
|
||||
const asyncIdentity = async <T>(v: T): Promise<T> => v;
|
||||
const identity = <T>(v: T): T => v;
|
||||
|
||||
it('works with async', async () => {
|
||||
const { executionTime, result } = await timedExecution(() => asyncIdentity(5));
|
||||
expect(result).toBe(5);
|
||||
// execution time is always positive
|
||||
expect(executionTime).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
it('works with sync', async () => {
|
||||
const { executionTime, result } = await timedExecution(() => identity(5));
|
||||
expect(result).toBe(5);
|
||||
// execution time is always positive
|
||||
expect(executionTime).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
@ -1,45 +1,5 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
||||
import path from 'path';
|
||||
import bunyan from 'bunyan';
|
||||
|
||||
export const log = bunyan.createLogger({
|
||||
name: 'fotochallenge',
|
||||
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
|
||||
src: true,
|
||||
});
|
||||
|
||||
const safePath = (basePath: string, name: string): boolean => {
|
||||
const fullPath = `${basePath}/${name}`;
|
||||
const relative = path.relative(basePath, fullPath);
|
||||
return (
|
||||
!!relative &&
|
||||
// does move out of `basePath`
|
||||
!relative.startsWith(`..${path.sep}`) &&
|
||||
// exactly one layer deep, e.g. no `./uplodas/foo/bar`
|
||||
!relative.includes(path.sep) &&
|
||||
// result is not an absolute path
|
||||
!path.isAbsolute(relative)
|
||||
);
|
||||
};
|
||||
const safePath = (input: string) => input.replace(/\W/g, '');
|
||||
|
||||
export default safePath;
|
||||
|
||||
const defaultPath: string = './uploads';
|
||||
if (!('STORAGE_PATH' in process.env)) {
|
||||
log.warn(`'STORAGE_PATH' environment variable is not set. Defaulting to ${defaultPath}`);
|
||||
}
|
||||
export const storagePath: string = process.env.STORAGE_PATH ?? defaultPath;
|
||||
|
||||
export const requestIdHeader = 'x-request-id';
|
||||
|
||||
export type MaybePromise<T> = T | Promise<T>;
|
||||
|
||||
export async function timedExecution<T>(
|
||||
fn: () => MaybePromise<T>
|
||||
): Promise<{ executionTime: number; result: T }> {
|
||||
const start = process.hrtime();
|
||||
const result = await fn();
|
||||
const end = process.hrtime(start);
|
||||
const executionTime = (end[0] * 1e6 + end[1]) / 1e6;
|
||||
return { executionTime, result };
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
||||
import { fail } from '@sveltejs/kit';
|
||||
import type { RequestEvent } from './$types';
|
||||
import safePath, { storagePath, log } from '$lib';
|
||||
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);
|
||||
@ -12,44 +14,29 @@ const mkdirIfNotExists = (path: string) => {
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
default: async ({ request, locals }: RequestEvent) => {
|
||||
let context: { [key: string]: string | string[] } = { requestId: locals.requestId };
|
||||
default: async ({ request }: RequestEvent) => {
|
||||
const data = await request.formData();
|
||||
|
||||
const formFiles = data.getAll('files');
|
||||
if (!formFiles) {
|
||||
log.debug(context, 'missing files');
|
||||
return fail(400, { field: 'files', files: formFiles, missing: true });
|
||||
} else if (!(formFiles as File[])) {
|
||||
log.debug(context, 'invalid files');
|
||||
return fail(400, { field: 'files', files: formFiles, incorrect: true });
|
||||
}
|
||||
const files = formFiles as File[];
|
||||
const fileNames = files.map((file) => file.name);
|
||||
context = { fileNames, ...context };
|
||||
console.log(files);
|
||||
if (files.length === 0) {
|
||||
log.debug(context, 'empty files');
|
||||
return fail(400, { field: 'files', files: formFiles, empty: true });
|
||||
}
|
||||
|
||||
const formName = data.get('name');
|
||||
if (!formName) {
|
||||
log.debug(context, 'missing name');
|
||||
return fail(400, { field: 'name', name: formName, missing: true });
|
||||
} else if (!(formName as string)) {
|
||||
log.debug(context, 'invalid name');
|
||||
return fail(400, { field: 'name', name: formName, incorrect: true });
|
||||
}
|
||||
|
||||
const name = formName as string;
|
||||
context = { name, ...context };
|
||||
|
||||
if (!safePath(storagePath, name)) {
|
||||
log.warn(context, 'Supplied name would cause dir traversal. Rejecting...');
|
||||
return fail(400, { field: 'name', name: name, incorrect: true });
|
||||
}
|
||||
|
||||
log.info(context, 'Uploading files');
|
||||
const name = safePath(formName as string);
|
||||
|
||||
files.forEach(async (file) => {
|
||||
const outPath = `${storagePath}/${name}`;
|
||||
@ -57,19 +44,17 @@ export const actions = {
|
||||
const ext = path.extname(file.name);
|
||||
|
||||
mkdirIfNotExists(outPath);
|
||||
const filename = `${hash('sha1', content)}${ext}`;
|
||||
const fullPath = `${outPath}/${filename}`;
|
||||
context = { file: fullPath, ...context };
|
||||
const filename = hash('sha1', content);
|
||||
const fullPath = `${outPath}/${filename}${ext}`;
|
||||
if (existsSync(fullPath)) {
|
||||
log.debug(context, 'File has already been uploaded. Skipping...');
|
||||
console.warn(`${fullPath} has already been uploaded. Skipping...`);
|
||||
} else {
|
||||
log.debug(context, 'saving file');
|
||||
writeFileSync(fullPath, Buffer.from(await file.arrayBuffer()), { flag: 'a+' });
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
success: true
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
|
@ -1,19 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import type { ActionData } from './$types';
|
||||
// @ts-expect-error: package does not contain type definitions
|
||||
import AutoComplete from 'simple-svelte-autocomplete';
|
||||
|
||||
export let form: ActionData;
|
||||
|
||||
let selectedName: string;
|
||||
|
||||
let files: FileList;
|
||||
let sending = false;
|
||||
|
||||
const siPrefixes = new Map([
|
||||
[1_000_000, 'M'],
|
||||
[1_000, 'k'],
|
||||
[1_000, 'k']
|
||||
]);
|
||||
const fileSize = (files: FileList) => {
|
||||
const size = Array.from(files)
|
||||
@ -25,12 +21,6 @@
|
||||
.map(([k, v]) => `${(size / k).toFixed(1)} ${v}B`)[0] ?? `${size} bytes`
|
||||
);
|
||||
};
|
||||
|
||||
async function loadNames() {
|
||||
const url = './names';
|
||||
const response = await fetch(url);
|
||||
return await response.json();
|
||||
}
|
||||
</script>
|
||||
|
||||
<form
|
||||
@ -54,15 +44,14 @@
|
||||
<div class="field">
|
||||
<label for="name" class="label">Name</label>
|
||||
<div class="control">
|
||||
<AutoComplete
|
||||
<input
|
||||
id="name"
|
||||
class="input"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Name"
|
||||
create={true}
|
||||
bind:selectedItem={selectedName}
|
||||
bind:text={selectedName}
|
||||
createText=""
|
||||
searchFunction={loadNames}
|
||||
value={form?.name ?? ''}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{#if form?.field === 'name'}
|
||||
@ -80,10 +69,10 @@
|
||||
<span class="file-cta">
|
||||
<span class="file-label">Fotos auswählen...</span>
|
||||
</span>
|
||||
{#if files && files?.length !== 0}
|
||||
<span class="file-name">
|
||||
{files.length} Bild{#if files.length > 1}er{/if} ausgewählt ({fileSize(files)})
|
||||
</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}
|
||||
|
@ -1,50 +0,0 @@
|
||||
import { storagePath } from '$lib';
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { stat, access, constants } from 'fs/promises';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
type Status = 'OK' | 'ERROR';
|
||||
type Result = { status: Status; checks: Checks };
|
||||
|
||||
type Checks = { [key: string]: CheckResult };
|
||||
type CheckResult = true | string;
|
||||
|
||||
const fileExists = async (path: string) => !!(await stat(path).catch(() => false));
|
||||
const isDirectory = async (path: string) =>
|
||||
!!(await stat(path)
|
||||
.then((s) => s.isDirectory())
|
||||
.catch(() => false));
|
||||
const isWritable = async (path: string) =>
|
||||
await access(path, constants.W_OK)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
const checkStoragePath = async (path: string): Promise<CheckResult> => {
|
||||
if (!fileExists(path)) {
|
||||
return '`STORAGE_PATH` does not exist';
|
||||
} else if (!isDirectory(path)) {
|
||||
return '`STORAGE_PATH` is not a directory';
|
||||
} else if (!isWritable(path)) {
|
||||
return '`STORAGE_PATH` is not writable';
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const GET: RequestHandler = async () => {
|
||||
const storagePathResult = await checkStoragePath(storagePath);
|
||||
|
||||
const checks: Checks = {
|
||||
storagePath: storagePathResult,
|
||||
};
|
||||
|
||||
const healthy = Object.values(checks)
|
||||
.map((r) => (r === true ? true : false))
|
||||
.reduce((prev, next) => prev && next, true);
|
||||
|
||||
const status = healthy ? 'OK' : 'ERROR';
|
||||
|
||||
const result: Result = { status, checks };
|
||||
const httpStatus = healthy ? 200 : 500;
|
||||
|
||||
return json(result, { status: httpStatus, headers: { healthy: healthy.toString() } });
|
||||
};
|
@ -1,12 +0,0 @@
|
||||
import { storagePath } from '$lib';
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { readdirSync, statSync } from 'fs';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const GET: RequestHandler = () => {
|
||||
const names = readdirSync(storagePath)
|
||||
.filter((f) => statSync(`${storagePath}/${f}`).isDirectory())
|
||||
.toSorted();
|
||||
|
||||
return json(names);
|
||||
};
|
@ -1,2 +1,2 @@
|
||||
/* Set your brand colors */
|
||||
$pink: #d47479;
|
||||
$pink: pink;
|
||||
|
@ -11,8 +11,8 @@ const config = {
|
||||
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||
adapter: adapter(),
|
||||
},
|
||||
adapter: adapter()
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
@ -1,13 +0,0 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test('healthy test', async ({ playwright }) => {
|
||||
const context = await playwright.request.newContext();
|
||||
const response = await context.get('health');
|
||||
await expect(response.status()).toBe(200);
|
||||
await expect(response.headers()).toHaveProperty('healthy');
|
||||
await expect(response.headers()['healthy']).toBe('true');
|
||||
|
||||
const body = await response.json();
|
||||
await expect(body).toHaveProperty('status');
|
||||
await expect(body['status']).toBe('OK');
|
||||
});
|
@ -1,10 +0,0 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test('contains header text', async ({ playwright }) => {
|
||||
const context = await playwright.request.newContext();
|
||||
const response = await context.get('');
|
||||
await expect(response.status()).toBe(200);
|
||||
|
||||
const body = (await response.body()).toString();
|
||||
await expect(body).toContain('Gabi und Hannes Fotochallenge');
|
||||
});
|
@ -1,37 +0,0 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { validate, NIL } from 'uuid';
|
||||
import { requestIdHeader } from '$lib';
|
||||
|
||||
test('response contains request-id header', async ({ playwright }) => {
|
||||
const context = await playwright.request.newContext();
|
||||
const response = await context.get('health');
|
||||
const headers = response.headers();
|
||||
await expect(headers).toHaveProperty(requestIdHeader);
|
||||
});
|
||||
|
||||
test('request-id is valid uuid', async ({ playwright }) => {
|
||||
const context = await playwright.request.newContext();
|
||||
const response = await context.get('health');
|
||||
const headers = response.headers();
|
||||
const requestId = headers[requestIdHeader];
|
||||
await expect(validate(requestId)).toBe(true);
|
||||
});
|
||||
|
||||
test('reuse valid incoming uuid', async ({ playwright }) => {
|
||||
const context = await playwright.request.newContext();
|
||||
|
||||
const response = await context.get('health', { headers: { [requestIdHeader]: NIL } });
|
||||
const headers = response.headers();
|
||||
const requestId = headers[requestIdHeader];
|
||||
await expect(requestId).toBe(NIL);
|
||||
});
|
||||
|
||||
test('ignore invalid incoming uuid', async ({ playwright }) => {
|
||||
const invalid = '00000000-0000-0000-0000-00000000000z';
|
||||
const context = await playwright.request.newContext();
|
||||
|
||||
const response = await context.get('health', { headers: { [requestIdHeader]: invalid } });
|
||||
const headers = response.headers();
|
||||
const requestId = headers[requestIdHeader];
|
||||
await expect(requestId).not.toBe(invalid);
|
||||
});
|
@ -5,14 +5,14 @@ export default defineConfig({
|
||||
plugins: [sveltekit()],
|
||||
|
||||
test: {
|
||||
include: ['src/**/*.{test,spec}.{js,ts}'],
|
||||
include: ['src/**/*.{test,spec}.{js,ts}']
|
||||
},
|
||||
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
additionalData: '@use "src/variables.scss" as *;',
|
||||
},
|
||||
},
|
||||
},
|
||||
additionalData: '@use "src/variables.scss" as *;'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
Reference in New Issue
Block a user