init: repo

This commit is contained in:
b1ek 2024-05-11 19:46:23 +10:00
commit 5a415586b7
Signed by: blek
GPG Key ID: 14546221E3595D0C
17 changed files with 1660 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
dist

2
.prettierignore Normal file
View File

@ -0,0 +1,2 @@
node_modules
dist

4
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
}

1
README.md Normal file
View File

@ -0,0 +1 @@
# backupd

10
config.json Normal file
View File

@ -0,0 +1,10 @@
{
"clients": [
{
"name": "izz",
"keySha256Sum": "eeb36e726e3ffec16da7798415bb4e531bf8a57fbe276fcc3fc6ea986cb02e9a",
"quota": 512
}
],
"storePath": "store"
}

24
eslint.config.js Normal file
View File

@ -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',
},
});

26
package.json Normal file
View File

@ -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"
}
}

11
prettier.config.js Normal file
View File

@ -0,0 +1,11 @@
/**
* @type { import("prettier").Config }
*/
export default {
trailingComma: 'all',
tabWidth: 4,
semi: true,
singleQuote: true,
endOfLine: 'lf'
}

18
scripts/build.js Normal file
View File

@ -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',
});

61
src/config.ts Normal file
View File

@ -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();

28
src/errors.ts Normal file
View File

@ -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;

19
src/index.ts Normal file
View File

@ -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,
});

6
src/routes/index.ts Normal file
View File

@ -0,0 +1,6 @@
import { FastifyPluginAsync } from 'fastify';
import upload from './upload.js';
export default (async function (fastify) {
await fastify.register(upload);
} as FastifyPluginAsync);

64
src/routes/upload.ts Normal file
View File

@ -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);

16
src/store.ts Normal file
View File

@ -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);
}

1
store/izz-testy Normal file
View File

@ -0,0 +1 @@
uwu

1367
yarn.lock Normal file

File diff suppressed because it is too large Load Diff