Compare commits
No commits in common. "6d68a704a36cc0e30e1f59df6f2654409ad25d8f" and "89a6efb4a3a517b5ae966e260a148b10b1001763" have entirely different histories.
6d68a704a3
...
89a6efb4a3
|
@ -1,5 +1,2 @@
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
store
|
|
||||||
config.json
|
|
||||||
docker-compose.yml
|
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
{
|
|
||||||
"clients": [
|
|
||||||
{
|
|
||||||
"name": "user",
|
|
||||||
"keySha256Sum": "0000000000000000000000000000000000000000000000000000000000000000",
|
|
||||||
"quota": 512
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"storePath": "store"
|
|
||||||
}
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"clients": [
|
||||||
|
{
|
||||||
|
"name": "izz",
|
||||||
|
"keySha256Sum": "eeb36e726e3ffec16da7798415bb4e531bf8a57fbe276fcc3fc6ea986cb02e9a",
|
||||||
|
"quota": 512
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storePath": "store"
|
||||||
|
}
|
|
@ -1,11 +0,0 @@
|
||||||
services:
|
|
||||||
server:
|
|
||||||
image: node:18-alpine
|
|
||||||
volumes:
|
|
||||||
- '.:/opt/code'
|
|
||||||
working_dir: '/opt/code'
|
|
||||||
entrypoint: ['/opt/code/docker-start.sh']
|
|
||||||
ports:
|
|
||||||
- 8080:80
|
|
||||||
environment:
|
|
||||||
HOST: 0.0.0.0
|
|
|
@ -1,4 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
yarn
|
|
||||||
yarn start
|
|
|
@ -1,23 +0,0 @@
|
||||||
import * as crypto from 'node:crypto';
|
|
||||||
|
|
||||||
export type Client = {
|
|
||||||
name: string;
|
|
||||||
keySha256Sum: string;
|
|
||||||
/**
|
|
||||||
* In megabytes
|
|
||||||
*/
|
|
||||||
quota?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type NoKeyClient = {
|
|
||||||
name: string;
|
|
||||||
quota?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function checkKey(key: string, client: Client): boolean {
|
|
||||||
const sha = crypto.createHash('sha256');
|
|
||||||
sha.update(key);
|
|
||||||
const hashed = sha.digest().toString('hex');
|
|
||||||
|
|
||||||
return client.keySha256Sum === hashed;
|
|
||||||
}
|
|
|
@ -1,9 +1,5 @@
|
||||||
import { Client, checkKey } from './client.js';
|
import fsp from 'node:fs/promises';
|
||||||
|
|
||||||
import * as fsp from 'node:fs/promises';
|
|
||||||
import Ajv from 'ajv';
|
import Ajv from 'ajv';
|
||||||
import { existsSync } from 'node:fs';
|
|
||||||
// @ts-expect-error: esnext moment
|
|
||||||
const ajv = new Ajv();
|
const ajv = new Ajv();
|
||||||
|
|
||||||
const config_validate = ajv.compile({
|
const config_validate = ajv.compile({
|
||||||
|
@ -27,16 +23,18 @@ const config_validate = ajv.compile({
|
||||||
});
|
});
|
||||||
|
|
||||||
export class Config {
|
export class Config {
|
||||||
clients: Client[];
|
clients: {
|
||||||
|
name: string;
|
||||||
|
keySha256Sum: string;
|
||||||
|
/**
|
||||||
|
* In megabytes
|
||||||
|
*/
|
||||||
|
quota?: number;
|
||||||
|
}[];
|
||||||
storePath: string;
|
storePath: string;
|
||||||
|
|
||||||
static async load(): Promise<Config> {
|
static async load(): Promise<Config> {
|
||||||
const path = process.env.CONFIG_PATH ?? 'config.json';
|
const path = process.env.CONFIG_PATH ?? 'config.json';
|
||||||
|
|
||||||
if (!existsSync(path)) {
|
|
||||||
throw new Error(`The config file ${path} does not exist`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const raw = JSON.parse(
|
const raw = JSON.parse(
|
||||||
await fsp.readFile(path, { encoding: 'utf8' }),
|
await fsp.readFile(path, { encoding: 'utf8' }),
|
||||||
) as Config;
|
) as Config;
|
||||||
|
@ -58,10 +56,6 @@ export class Config {
|
||||||
|
|
||||||
return raw;
|
return raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getClientByKey(config: Config, key: string): Client | undefined {
|
|
||||||
return config.clients.find((client) => checkKey(key, client));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const config = await Config.load();
|
export const config = await Config.load();
|
||||||
|
|
|
@ -26,21 +26,3 @@ export const InvalidAuthorization = () =>
|
||||||
name: 'InvalidAuthorization',
|
name: 'InvalidAuthorization',
|
||||||
message: 'Provided authorization credentials are invalid',
|
message: 'Provided authorization credentials are invalid',
|
||||||
}) as FastifyError;
|
}) as FastifyError;
|
||||||
|
|
||||||
export const QuotaExceeded = () =>
|
|
||||||
({
|
|
||||||
code: '422',
|
|
||||||
statusCode: 422,
|
|
||||||
name: 'QuotaExceeded',
|
|
||||||
message:
|
|
||||||
'Your quota has exceeded. Please delete some files to proceed.',
|
|
||||||
}) as FastifyError;
|
|
||||||
|
|
||||||
export const NotFoundError = () =>
|
|
||||||
({
|
|
||||||
code: '404',
|
|
||||||
statusCode: 404,
|
|
||||||
name: 'NotFound',
|
|
||||||
message:
|
|
||||||
'The requested resource has never existed, deleted or has a different name or path.',
|
|
||||||
}) as FastifyError;
|
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
import { type FastifyPluginAsync } from 'fastify';
|
|
||||||
import * as fsp from 'node:fs/promises';
|
|
||||||
|
|
||||||
import { Config, config } from '../config.js';
|
|
||||||
import { InvalidAuthorization } from '../errors.js';
|
|
||||||
import { getFilesFor, getPathFor } from '../store.js';
|
|
||||||
|
|
||||||
export default (async function (fastify) {
|
|
||||||
fastify.delete('/:name', async (req) => {
|
|
||||||
const key = req.headers.authorization?.replace('Bearer ', '') ?? 'none';
|
|
||||||
|
|
||||||
const client = Config.getClientByKey(config, key);
|
|
||||||
if (!client) {
|
|
||||||
throw InvalidAuthorization();
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = req.params as { name: string };
|
|
||||||
const files = await getFilesFor(client.name);
|
|
||||||
|
|
||||||
if (!files.find((x) => x === client.name + '-' + params.name)) {
|
|
||||||
return { status: 'ok' };
|
|
||||||
}
|
|
||||||
|
|
||||||
await fsp.rm(getPathFor(params.name, client.name));
|
|
||||||
|
|
||||||
return { status: 'ok' };
|
|
||||||
});
|
|
||||||
} as FastifyPluginAsync);
|
|
|
@ -1,30 +0,0 @@
|
||||||
import { FastifyPluginAsync } from 'fastify';
|
|
||||||
import * as fsp from 'node:fs/promises';
|
|
||||||
|
|
||||||
import { Config, config } from '../config.js';
|
|
||||||
import { InvalidAuthorization, NotFoundError } from '../errors.js';
|
|
||||||
import { getFilesFor, getPathFor } from '../store.js';
|
|
||||||
|
|
||||||
export default (async function (fastify) {
|
|
||||||
fastify.get('/get/:name', async (req, rep) => {
|
|
||||||
const key = req.headers.authorization?.replace('Bearer ', '') ?? 'none';
|
|
||||||
|
|
||||||
const client = Config.getClientByKey(config, key);
|
|
||||||
if (!client) {
|
|
||||||
throw InvalidAuthorization();
|
|
||||||
}
|
|
||||||
|
|
||||||
const files = (await getFilesFor(client.name)).map((x) =>
|
|
||||||
x.replace(client.name + '-', ''),
|
|
||||||
);
|
|
||||||
|
|
||||||
const params = req.params as { name: string };
|
|
||||||
|
|
||||||
if (!files.find((x) => x === params.name)) {
|
|
||||||
throw NotFoundError();
|
|
||||||
}
|
|
||||||
|
|
||||||
rep.type('application/octet-stream');
|
|
||||||
return await fsp.readFile(getPathFor(params.name, client.name));
|
|
||||||
});
|
|
||||||
} as FastifyPluginAsync);
|
|
|
@ -1,14 +1,6 @@
|
||||||
import { FastifyPluginAsync } from 'fastify';
|
import { FastifyPluginAsync } from 'fastify';
|
||||||
import deleter from './delete.js';
|
|
||||||
import upload from './upload.js';
|
import upload from './upload.js';
|
||||||
import index from './indexpage.js';
|
|
||||||
import list from './list.js';
|
|
||||||
import get from './get.js';
|
|
||||||
|
|
||||||
export default (async function (fastify) {
|
export default (async function (fastify) {
|
||||||
await fastify.register(deleter);
|
|
||||||
await fastify.register(upload);
|
await fastify.register(upload);
|
||||||
await fastify.register(index);
|
|
||||||
await fastify.register(list);
|
|
||||||
await fastify.register(get);
|
|
||||||
} as FastifyPluginAsync);
|
} as FastifyPluginAsync);
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
import { FastifyPluginAsync } from 'fastify';
|
|
||||||
|
|
||||||
const page = `<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Backup server</title>
|
|
||||||
<style>*{background:#111;color:white}a{color:#abf}</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>You have reached the backup server!</h1>
|
|
||||||
<p>This is the backup server! If you are a regular user, you wouldn't find this place very interesting and might as well close this page now.</p>
|
|
||||||
<p>If you are a sysadmin, please refer to <a href='https://git.blek.codes/blek/backups.git'>API docs</a> for more info</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`
|
|
||||||
.replaceAll('\n', '')
|
|
||||||
.replaceAll(/ +/gm, ' ')
|
|
||||||
.replaceAll(' <', '<');
|
|
||||||
|
|
||||||
export default (async function (fastify) {
|
|
||||||
fastify.get('/', async (_req, rep) => {
|
|
||||||
rep.type('text/html; charset=utf8');
|
|
||||||
return page;
|
|
||||||
});
|
|
||||||
fastify.get('/favicon.ico', async (req, rep) => {
|
|
||||||
rep.status(404);
|
|
||||||
return '';
|
|
||||||
});
|
|
||||||
} as FastifyPluginAsync);
|
|
|
@ -1,41 +0,0 @@
|
||||||
import { type FastifyPluginAsync } from 'fastify';
|
|
||||||
import * as fsp from 'node:fs/promises';
|
|
||||||
import * as path from 'node:path';
|
|
||||||
import { Stats } from 'node:fs';
|
|
||||||
|
|
||||||
import { Config, config } from '../config.js';
|
|
||||||
import { InvalidAuthorization } from '../errors.js';
|
|
||||||
|
|
||||||
type ListFilesResponse = {
|
|
||||||
name: string;
|
|
||||||
created: Date;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default (async function (fastify) {
|
|
||||||
fastify.get('/list', async (req) => {
|
|
||||||
const key = req.headers.authorization?.replace('Bearer ', '') ?? 'none';
|
|
||||||
|
|
||||||
const client = Config.getClientByKey(config, key);
|
|
||||||
if (!client) {
|
|
||||||
throw InvalidAuthorization();
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawfl = await fsp.readdir(config.storePath);
|
|
||||||
const files: [Stats, string][] = await Promise.all(
|
|
||||||
rawfl
|
|
||||||
.filter((x) => x.startsWith(client.name + '-'))
|
|
||||||
.map(async (x) => [
|
|
||||||
await fsp.stat(path.join(config.storePath, x)),
|
|
||||||
x,
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
return files.map(
|
|
||||||
(x) =>
|
|
||||||
({
|
|
||||||
name: x[1].replace(client.name + '-', ''),
|
|
||||||
created: x[0].birthtime,
|
|
||||||
}) as ListFilesResponse,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
} as FastifyPluginAsync);
|
|
|
@ -1,20 +1,19 @@
|
||||||
import { FastifyPluginAsync } from 'fastify';
|
import { FastifyPluginAsync } from 'fastify';
|
||||||
import Ajv, { JSONSchemaType, type Schema } from 'ajv';
|
import Ajv, { JSONSchemaType, type Schema } from 'ajv';
|
||||||
|
import crypto from 'node:crypto';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
InvalidAuthorization,
|
InvalidAuthorization,
|
||||||
InvalidPayload,
|
InvalidPayload,
|
||||||
QuotaExceeded,
|
|
||||||
ValidationError,
|
ValidationError,
|
||||||
} from '../errors.js';
|
} from '../errors.js';
|
||||||
import { Config, config } from '../config.js';
|
import { config } from '../config.js';
|
||||||
import { save } from '../store.js';
|
import { getTakenQuota, save } from '../store.js';
|
||||||
|
|
||||||
type UploadPayload = {
|
type UploadPayload = {
|
||||||
data: string; // base64-encoded
|
data: string; // base64-encoded
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
// @ts-expect-error: esnext moment
|
|
||||||
const ajv = new Ajv();
|
const ajv = new Ajv();
|
||||||
const upload_schema = {
|
const upload_schema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
@ -27,9 +26,11 @@ const upload_schema = {
|
||||||
|
|
||||||
export default (async function (fastify) {
|
export default (async function (fastify) {
|
||||||
fastify.post('/upload', async (req) => {
|
fastify.post('/upload', async (req) => {
|
||||||
const key = req.headers.authorization?.replace('Bearer ', '') ?? 'none';
|
const sha = crypto.createHash('sha256');
|
||||||
|
sha.update(req.headers.authorization?.replace('Bearer ', '') ?? 'none');
|
||||||
|
const key = sha.digest().toString('hex');
|
||||||
|
|
||||||
const client = Config.getClientByKey(config, key);
|
const client = config.clients.find((x) => x.keySha256Sum === key);
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
throw InvalidAuthorization();
|
throw InvalidAuthorization();
|
||||||
|
@ -53,10 +54,7 @@ export default (async function (fastify) {
|
||||||
const data = Buffer.from(body.data, 'base64');
|
const data = Buffer.from(body.data, 'base64');
|
||||||
const name = body.name;
|
const name = body.name;
|
||||||
|
|
||||||
const status = await save(data, name, client.name);
|
await save(data, name, client.name);
|
||||||
if (status === 'Quota exceeded') {
|
|
||||||
throw QuotaExceeded();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
|
|
24
src/store.ts
24
src/store.ts
|
@ -1,10 +1,11 @@
|
||||||
import { config } from './config.js';
|
import { config } from './config.js';
|
||||||
|
|
||||||
import * as path from 'path';
|
import path from 'path';
|
||||||
import * as fsp from 'node:fs/promises';
|
import fsp from 'node:fs/promises';
|
||||||
import { NoKeyClient } from './client.js';
|
|
||||||
|
|
||||||
function getClient(clientName: string): NoKeyClient | undefined {
|
function getClient(
|
||||||
|
clientName: string,
|
||||||
|
): { name: string; quota: number } | undefined {
|
||||||
const client = config.clients.find((x) => x.name === clientName) as
|
const client = config.clients.find((x) => x.name === clientName) as
|
||||||
| { name: string; quota: number; keySha256Sum?: string }
|
| { name: string; quota: number; keySha256Sum?: string }
|
||||||
| undefined;
|
| undefined;
|
||||||
|
@ -13,9 +14,13 @@ function getClient(clientName: string): NoKeyClient | undefined {
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getClientOrThrowErr(clientName: string, error?: string): NoKeyClient {
|
function getClientOrThrowErr(
|
||||||
|
clientName: string,
|
||||||
|
error?: string,
|
||||||
|
): { name: string; quota: number } {
|
||||||
const client = getClient(clientName);
|
const client = getClient(clientName);
|
||||||
if (!client) throw Error(error ?? 'Client does not exist.');
|
if (typeof client === 'undefined')
|
||||||
|
throw Error(error ?? 'Client does not exist.');
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,10 +56,9 @@ export async function save(
|
||||||
const client = getClientOrThrowErr(clientName);
|
const client = getClientOrThrowErr(clientName);
|
||||||
const quota = await getTakenQuota(clientName);
|
const quota = await getTakenQuota(clientName);
|
||||||
|
|
||||||
if (client.quota)
|
if (data.length + quota > client.quota) {
|
||||||
if (data.length + quota > client.quota) {
|
return 'Quota exceeded';
|
||||||
return 'Quota exceeded';
|
}
|
||||||
}
|
|
||||||
|
|
||||||
await fsp.writeFile(getPathFor(name, clientName), data);
|
await fsp.writeFile(getPathFor(name, clientName), data);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
uwu
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"lib": ["ES2023"],
|
|
||||||
"module": "NodeNext",
|
|
||||||
"moduleResolution": "NodeNext"
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue