init: repo
This commit is contained in:
commit
5a415586b7
|
@ -0,0 +1,2 @@
|
|||
node_modules
|
||||
dist
|
|
@ -0,0 +1,2 @@
|
|||
node_modules
|
||||
dist
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"clients": [
|
||||
{
|
||||
"name": "izz",
|
||||
"keySha256Sum": "eeb36e726e3ffec16da7798415bb4e531bf8a57fbe276fcc3fc6ea986cb02e9a",
|
||||
"quota": 512
|
||||
}
|
||||
],
|
||||
"storePath": "store"
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
// @ts-check
|
||||
|
||||
import tse from 'typescript-eslint';
|
||||
|
||||
export default tse.config(...tse.configs.recommended, {
|
||||
rules: {
|
||||
'no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '_',
|
||||
args: 'after-used',
|
||||
caughtErrors: 'none',
|
||||
},
|
||||
],
|
||||
'no-invalid-regexp': 'error',
|
||||
'no-var': 'error',
|
||||
eqeqeq: 'error',
|
||||
'no-useless-concat': 'error',
|
||||
'no-useless-rename': 'error',
|
||||
'no-useless-assignment': 'error',
|
||||
'default-case': 'error',
|
||||
'prefer-const': 'error',
|
||||
},
|
||||
});
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "fastify-ts-template",
|
||||
"license": "GPL-3.0-only",
|
||||
"author": "blek! <me@blek.codes>",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"dependencies": {
|
||||
"ajv": "^8.13.0",
|
||||
"fastify": "^4.27.0",
|
||||
"typescript-eslint": "^7.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/eslint": "^8.56.10",
|
||||
"@types/node": "^20.12.11",
|
||||
"esbuild": "^0.21.1",
|
||||
"eslint": "^9.2.0",
|
||||
"prettier": "3.2.5",
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "yarn build && node dist/index.js",
|
||||
"build": "yarn lint && node scripts/build.js",
|
||||
"lint": "eslint",
|
||||
"format": "prettier"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
|
||||
/**
|
||||
* @type { import("prettier").Config }
|
||||
*/
|
||||
export default {
|
||||
trailingComma: 'all',
|
||||
tabWidth: 4,
|
||||
semi: true,
|
||||
singleQuote: true,
|
||||
endOfLine: 'lf'
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import esbuild from 'esbuild';
|
||||
import fs from 'node:fs';
|
||||
|
||||
const entryPoints = fs
|
||||
.readdirSync('src', { recursive: true })
|
||||
.filter((x) => x.endsWith('.ts'))
|
||||
.map((x) => 'src/' + x);
|
||||
|
||||
if (fs.existsSync('dist')) fs.rmSync('dist', { recursive: true, force: true });
|
||||
|
||||
try {
|
||||
fs.mkdirSync('dist');
|
||||
} catch (_) {}
|
||||
|
||||
await esbuild.build({
|
||||
entryPoints,
|
||||
outdir: 'dist',
|
||||
});
|
|
@ -0,0 +1,61 @@
|
|||
import fsp from 'node:fs/promises';
|
||||
import Ajv from 'ajv';
|
||||
const ajv = new Ajv();
|
||||
|
||||
const config_validate = ajv.compile({
|
||||
type: 'object',
|
||||
properties: {
|
||||
clients: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
keySha256Sum: { type: 'string' },
|
||||
quota: { type: 'number', nullable: true },
|
||||
},
|
||||
required: ['name', 'keySha256Sum'],
|
||||
},
|
||||
},
|
||||
storePath: { type: 'string' },
|
||||
},
|
||||
required: ['clients', 'storePath'],
|
||||
});
|
||||
|
||||
export class Config {
|
||||
clients: {
|
||||
name: string;
|
||||
keySha256Sum: string;
|
||||
/**
|
||||
* In megabytes
|
||||
*/
|
||||
quota?: number;
|
||||
}[];
|
||||
storePath: string;
|
||||
|
||||
static async load(): Promise<Config> {
|
||||
const path = process.env.CONFIG_PATH ?? 'config.json';
|
||||
const raw = JSON.parse(
|
||||
await fsp.readFile(path, { encoding: 'utf8' }),
|
||||
) as Config;
|
||||
if (!config_validate(raw)) {
|
||||
throw (
|
||||
'Config invalid: ' +
|
||||
(config_validate.errors
|
||||
?.map((x) => x.keyword + ': ' + x.message)
|
||||
?.join('; ') ?? 'No validation errors provided')
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await fsp.mkdir(raw.storePath, { recursive: true });
|
||||
} catch (err) {
|
||||
console.error('Could not create storePath directory:');
|
||||
throw err;
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
export const config = await Config.load();
|
|
@ -0,0 +1,28 @@
|
|||
import { FastifyError } from 'fastify';
|
||||
|
||||
export const InvalidPayload = () =>
|
||||
({
|
||||
code: '400',
|
||||
statusCode: 400,
|
||||
name: 'InvalidPayload',
|
||||
message:
|
||||
"Payload type is invalid. Did you forget to set a proper 'Content-Type'?",
|
||||
}) as FastifyError;
|
||||
|
||||
export const ValidationError = (err?: string) =>
|
||||
({
|
||||
code: '400',
|
||||
statusCode: 400,
|
||||
name: 'ValidationError',
|
||||
message: err
|
||||
? 'The schema does not match'
|
||||
: 'The schema does not match: ' + err,
|
||||
}) as FastifyError;
|
||||
|
||||
export const InvalidAuthorization = () =>
|
||||
({
|
||||
code: '401',
|
||||
statusCode: 401,
|
||||
name: 'InvalidAuthorization',
|
||||
message: 'Provided authorization credentials are invalid',
|
||||
}) as FastifyError;
|
|
@ -0,0 +1,19 @@
|
|||
import Fastify from 'fastify';
|
||||
import routes from './routes/index.js';
|
||||
|
||||
const fastify = Fastify();
|
||||
|
||||
// register any plugins here, i.e.
|
||||
// fastify.register(middie);
|
||||
|
||||
fastify.register(routes);
|
||||
|
||||
const port = parseInt(process.env.PORT ?? '80');
|
||||
const host = process.env.HOST ?? 'localhost';
|
||||
|
||||
console.log(`Listening on ${host}:${port}`);
|
||||
|
||||
fastify.listen({
|
||||
port,
|
||||
host,
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
import { FastifyPluginAsync } from 'fastify';
|
||||
import upload from './upload.js';
|
||||
|
||||
export default (async function (fastify) {
|
||||
await fastify.register(upload);
|
||||
} as FastifyPluginAsync);
|
|
@ -0,0 +1,64 @@
|
|||
import { FastifyPluginAsync } from 'fastify';
|
||||
import Ajv, { JSONSchemaType, type Schema } from 'ajv';
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
import {
|
||||
InvalidAuthorization,
|
||||
InvalidPayload,
|
||||
ValidationError,
|
||||
} from '../errors.js';
|
||||
import { config } from '../config.js';
|
||||
import { save } from '../store.js';
|
||||
|
||||
type UploadPayload = {
|
||||
data: string; // base64-encoded
|
||||
name: string;
|
||||
};
|
||||
const ajv = new Ajv();
|
||||
const upload_schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
data: { type: 'string' },
|
||||
name: { type: 'string' },
|
||||
},
|
||||
required: ['data', 'name'],
|
||||
} as Schema | JSONSchemaType<UploadPayload>;
|
||||
|
||||
export default (async function (fastify) {
|
||||
fastify.post('/upload', async (req) => {
|
||||
const sha = crypto.createHash('sha256');
|
||||
sha.update(req.headers.authorization?.replace('Bearer ', '') ?? 'none');
|
||||
const key = sha.digest().toString('hex');
|
||||
|
||||
const client = config.clients.find((x) => x.keySha256Sum === key);
|
||||
|
||||
if (!client) {
|
||||
throw InvalidAuthorization();
|
||||
}
|
||||
|
||||
const body = req.body as UploadPayload;
|
||||
|
||||
if (typeof body !== 'object') {
|
||||
throw InvalidPayload();
|
||||
}
|
||||
|
||||
const compiled = ajv.compile(upload_schema);
|
||||
if (!compiled(body)) {
|
||||
throw ValidationError(
|
||||
compiled.errors
|
||||
?.map((x) => x.keyword + ': ' + x.message)
|
||||
.join('; '),
|
||||
);
|
||||
}
|
||||
|
||||
const data = Buffer.from(body.data, 'base64');
|
||||
const name = body.name;
|
||||
|
||||
await save(data, name, client.name);
|
||||
|
||||
return {
|
||||
status: 'ok',
|
||||
name,
|
||||
};
|
||||
});
|
||||
} as FastifyPluginAsync);
|
|
@ -0,0 +1,16 @@
|
|||
import { config } from './config.js';
|
||||
|
||||
import path from 'path';
|
||||
import fsp from 'node:fs/promises';
|
||||
|
||||
export function getPathFor(name: string, clientName: string): string {
|
||||
return path.join(config.storePath, clientName + '-' + name);
|
||||
}
|
||||
|
||||
export async function save(
|
||||
data: string | Buffer | NodeJS.ArrayBufferView,
|
||||
name: string,
|
||||
clientName: string,
|
||||
): Promise<undefined> {
|
||||
await fsp.writeFile(getPathFor(name, clientName), data);
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
uwu
|
Loading…
Reference in New Issue