Skip to content
This repository has been archived by the owner on Feb 9, 2025. It is now read-only.

Add mongodb support #247

Closed
wants to merge 11 commits into from
10 changes: 5 additions & 5 deletions backend/UserConfig.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { UserConfiguration, UserConfigTypeChecker, PostgresConfiguration } from 'ass';
import { UserConfiguration, UserConfigTypeChecker, PostgresConfiguration, MongoDBConfiguration } from 'ass';

import fs from 'fs-extra';
import { path } from '@tycrek/joint';
Expand Down Expand Up @@ -108,15 +108,15 @@ export class UserConfig {
// * Optional database config(s)
if (config.database != null) {
// these both have the same schema so we can just check both
if (config.database.kind == 'mysql' || config.database.kind == 'postgres') {
if (config.database.kind == 'mysql' || config.database.kind == 'postgres' || config.database.kind == 'mongodb') {
if (config.database.options != undefined) {
if (!Checkers.sql.mySql.host(config.database.options.host)) throw new Error('Invalid database host');
if (!Checkers.sql.mySql.user(config.database.options.user)) throw new Error('Invalid databse user');
if (!Checkers.sql.mySql.password(config.database.options.password)) throw new Error('Invalid database password');
if (!Checkers.sql.mySql.database(config.database.options.database)) throw new Error('Invalid database');
if (config.database.kind == 'postgres') {
if (!Checkers.sql.postgres.port((config.database.options as PostgresConfiguration).port)) {
throw new Error("Invalid database port");
if (config.database.kind == 'postgres' || config.database.kind == 'mongodb') {
if (!Checkers.sql.postgres.port((config.database.options as PostgresConfiguration | MongoDBConfiguration).port)) {
throw new Error('Invalid database port');
}
}
} else throw new Error('Database options missing');
Expand Down
4 changes: 4 additions & 0 deletions backend/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { DBManager } from './sql/database.js';
import { JSONDatabase } from './sql/json.js';
import { MySQLDatabase } from './sql/mysql.js';
import { PostgreSQLDatabase } from './sql/postgres.js';
import { MongoDBDatabase } from './sql/mongodb.js';
import { buildFrontendRouter } from './routers/_frontend.js';

/**
Expand Down Expand Up @@ -128,6 +129,9 @@ async function main() {
case 'postgres':
await DBManager.use(new PostgreSQLDatabase());
break;
case 'mongodb':
await DBManager.use(new MongoDBDatabase());
break;
}
} catch (err) { throw new Error(`Failed to configure SQL`); }
} else { // default to json database
Expand Down
1 change: 1 addition & 0 deletions backend/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type DataSector = 'files' | 'users';
const DBNAMES = {
'mysql': 'MySQL',
'postgres': 'PostgreSQL',
'mongodb': 'MongoDB',
'json': 'JSON'
};

Expand Down
10 changes: 7 additions & 3 deletions backend/routers/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { DBManager } from '../sql/database.js';
import { JSONDatabase } from '../sql/json.js';
import { MySQLDatabase } from '../sql/mysql.js';
import { PostgreSQLDatabase } from '../sql/postgres.js';
import { MongoDBDatabase } from '../sql/mongodb.js';

const router = Router({ caseSensitive: true });

Expand Down Expand Up @@ -41,13 +42,16 @@ router.post('/setup', BodyParserJson(), async (req, res) => {
case 'postgres':
await DBManager.use(new PostgreSQLDatabase());
break;
case 'mongodb':
await DBManager.use(new MongoDBDatabase());
break;
}
}

// set rate limits
if (UserConfig.config.rateLimit?.api) setRateLimiter('api', UserConfig.config.rateLimit.api);
if (UserConfig.config.rateLimit?.login) setRateLimiter('login', UserConfig.config.rateLimit.login);
if (UserConfig.config.rateLimit?.upload) setRateLimiter('upload', UserConfig.config.rateLimit.upload);;
if (UserConfig.config.rateLimit?.api) setRateLimiter('api', UserConfig.config.rateLimit.api);
if (UserConfig.config.rateLimit?.login) setRateLimiter('login', UserConfig.config.rateLimit.login);
if (UserConfig.config.rateLimit?.upload) setRateLimiter('upload', UserConfig.config.rateLimit.upload);

log.success('Setup', 'completed');

Expand Down
2 changes: 1 addition & 1 deletion backend/routers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ bb.extend(router, {
router.get('/', (req, res) => UserConfig.ready ? res.render('index', { version: App.pkgVersion }) : res.redirect('/setup'));

// Upload flow
router.post('/', rateLimiterMiddleware("upload", UserConfig.config?.rateLimit?.upload), async (req, res) => {
router.post('/', rateLimiterMiddleware('upload', UserConfig.config?.rateLimit?.upload), async (req, res) => {

// Check user config
if (!UserConfig.ready) return res.status(500).type('text').send('Configuration missing!');
Expand Down
10 changes: 5 additions & 5 deletions backend/sql/database.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AssFile, AssUser, NID, UploadToken } from "ass";
import { AssFile, AssUser, NID, UploadToken } from 'ass';

export type DatabaseValue = AssFile | AssUser | UploadToken;
export type DatabaseTable = 'assfiles' | 'assusers' | 'asstokens';
Expand Down Expand Up @@ -73,7 +73,7 @@ export class DBManager {
public static configure(): Promise<void> {
if (this._db && this._dbReady) {
return this._db.configure();
} else throw new Error("No database active");
} else throw new Error('No database active');
}

/**
Expand All @@ -82,7 +82,7 @@ export class DBManager {
public static put(table: DatabaseTable, key: NID, data: DatabaseValue): Promise<void> {
if (this._db && this._dbReady) {
return this._db.put(table, key, data);
} else throw new Error("No database active");
} else throw new Error('No database active');
}

/**
Expand All @@ -91,7 +91,7 @@ export class DBManager {
public static get(table: DatabaseTable, key: NID): Promise<DatabaseValue | undefined> {
if (this._db && this._dbReady) {
return this._db.get(table, key);
} else throw new Error("No database active");
} else throw new Error('No database active');
}

/**
Expand All @@ -100,6 +100,6 @@ export class DBManager {
public static getAll(table: DatabaseTable): Promise<{ [index: string]: DatabaseValue }> {
if (this._db && this._dbReady) {
return this._db.getAll(table);
} else throw new Error("No database active");
} else throw new Error('No database active');
}
}
240 changes: 240 additions & 0 deletions backend/sql/mongodb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
import { AssFile, AssUser, MongoDBConfiguration, NID, UploadToken } from 'ass';

import mongoose, { Model, Mongoose, Schema } from 'mongoose';

import { UserConfig } from '../UserConfig.js';
import { Database, DatabaseTable, DatabaseValue } from './database.js';
import { log } from '../log.js';

interface TableVersion {
name: string;
version: number;
}

const VERSIONS_SCHEMA = new Schema<TableVersion>({
name: String,
version: Number
});

interface MongoSchema<T> {
id: NID,
data: T
}

const FILE_SCHEMA = new Schema<MongoSchema<AssFile>>({
id: String,
data: {
fakeid: String,
fileKey: String,
filename: String,
mimetype: String,
save: {
local: String,
s3: Boolean // this will break if it gets the url object
// but im so fucking tired of this, were just
// going to keep it like this until it becomes
// a problem
},
sha256: String,
size: Number,
timestamp: String,
uploader: String
}
});

const TOKEN_SCHEMA = new Schema<MongoSchema<UploadToken>>({
id: String,
data: {
id: String,
token: String,
hint: String
}
});

const USER_SCHEMA = new Schema<MongoSchema<AssUser>>({
id: String,
data: {
id: String,
username: String,
password: String,
admin: Boolean,
tokens: [ String ],
files: [ String ],
meta: {
type: String,
get: (v: string) => JSON.parse(v),
set: (v: string) => JSON.stringify(v)
}
}
});

/**
* database adapter for mongodb
*/
export class MongoDBDatabase implements Database {
private _client: Mongoose;

// mongoose models
private _versionModel: Model<TableVersion>;
private _fileModel: Model<MongoSchema<AssFile>>;
private _tokenModel: Model<MongoSchema<UploadToken>>;
private _userModel: Model<MongoSchema<AssUser>>;

private _validateConfig(): string | undefined {
// make sure the configuration exists
if (!UserConfig.ready) return 'User configuration not ready';
if (typeof UserConfig.config.database != 'object') return 'MongoDB configuration missing';
if (UserConfig.config.database.kind != 'mongodb') return 'Database not set to MongoDB, but MongoDB is in use, something has gone terribly wrong';
if (typeof UserConfig.config.database.options != 'object') return 'MongoDB configuration missing';

let config = UserConfig.config.database.options;

// check the postgres config
const checker = (val: string) => val != null && val !== '';
const issue =
!checker(config.host) ? 'Missing MongoDB Host'
: !checker(config.user) ? 'Missing MongoDB User'
: !checker(config.password) ? 'Missing MongoDB Password'
: !checker(config.database) ? 'Missing MongoDB Database'
// ! Blame VS Code for this weird indentation
: undefined;

return issue;
}

open(): Promise<void> {
return new Promise(async (resolve, reject) => {
try {
// validate config
let configError = this._validateConfig();
if (configError != null) throw new Error(configError);

let options = UserConfig.config.database!.options! as MongoDBConfiguration;

// connect
log.info('MongoDB', `connecting to ${options.host}:${options.port}`);
this._client = await mongoose.connect(`mongodb://${options.user}:${options.password}@${options.host}:${options.port}/${options.database}`);
log.success('MongoDB', 'ok');

resolve();
} catch (err) {
log.error('MongoDB', 'failed to connect');
console.error(err);
reject(err);
}
});
}

close(): Promise<void> {
return new Promise(async (resolve, reject) => {
try {
// gracefully disconnect
await this._client.disconnect();

resolve();
} catch (err) {
log.error('MongoDB', 'failed to disconnect');
console.error(err);
reject(err);
}
});
}

configure(): Promise<void> {
return new Promise(async (resolve, reject) => {
try {
this._versionModel = this._client.model('assversions', VERSIONS_SCHEMA);
this._fileModel = this._client.model('assfiles', FILE_SCHEMA);
this._tokenModel = this._client.model('asstokens', TOKEN_SCHEMA);
this._userModel = this._client.model('assusers', USER_SCHEMA);

// theres only one version right now so we dont need to worry about anything, just adding the version thingies if they arent there
let versions = await this._versionModel.find().exec()
.then(res => res.reduce((obj, doc) => obj.set(doc.name, doc.version), new Map<string, number>()));

for (let [table, version] of [['assfiles', 1], ['asstokens', 1], ['assusers', 1]] as [string, number][]) {
if (!versions.has(table)) {
// set the version
new this._versionModel({
name: table,
version: version
}).save();

versions.set(table, version);
}
}

resolve();
} catch (err) {
log.error('MongoDB', 'failed to configure');
console.error(err);
reject(err);
}
});
}

put(table: DatabaseTable, key: string, data: DatabaseValue): Promise<void> {
return new Promise(async (resolve, reject) => {
try {
const models = {
assfiles: this._fileModel,
assusers: this._userModel,
asstokens: this._tokenModel
};

await new models[table]({
id: key,
data: data
}).save();

resolve();
} catch (err) {
reject(err);
}
});
}

get(table: DatabaseTable, key: string): Promise<DatabaseValue | undefined> {
return new Promise(async (resolve, reject) => {
try {
const models = {
assfiles: this._fileModel,
assusers: this._userModel,
asstokens: this._tokenModel
};

// @ts-ignore
// typescript cant infer this but it is 100% correct
// no need to worry :>
let result = await models[table].find({
id: key
}).exec();

resolve(result.length ? result[0].data : void 0);
} catch (err) {
reject(err);
}
});
}

getAll(table: DatabaseTable): Promise<{ [index: string]: DatabaseValue; }> {
return new Promise(async (resolve, reject) => {
try {
const models = {
assfiles: this._fileModel,
assusers: this._userModel,
asstokens: this._tokenModel
};

// more ts-ignore!
// @ts-ignore
let result = await models[table].find({}).exec() // @ts-ignore
.then(res => res.reduce((obj, doc) => (obj[doc.id] = doc.data, obj), {}));

resolve(result);
} catch (err) {
reject(err);
}
});
}
};
2 changes: 1 addition & 1 deletion backend/sql/mysql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class MySQLDatabase implements Database {
// make sure the configuration exists
if (!UserConfig.ready) return 'User configuration not ready';
if (typeof UserConfig.config.database != 'object') return 'MySQL configuration missing';
if (UserConfig.config.database.kind != "mysql") return 'Database not set to MySQL, but MySQL is in use, something has gone terribly wrong';
if (UserConfig.config.database.kind != 'mysql') return 'Database not set to MySQL, but MySQL is in use, something has gone terribly wrong';
if (typeof UserConfig.config.database.options != 'object') return 'MySQL configuration missing';

let mySqlConf = UserConfig.config.database.options;
Expand Down
3 changes: 1 addition & 2 deletions backend/sql/postgres.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class PostgreSQLDatabase implements Database {
// make sure the configuration exists
if (!UserConfig.ready) return 'User configuration not ready';
if (typeof UserConfig.config.database != 'object') return 'PostgreSQL configuration missing';
if (UserConfig.config.database.kind != "postgres") return 'Database not set to PostgreSQL, but PostgreSQL is in use, something has gone terribly wrong';
if (UserConfig.config.database.kind != 'postgres') return 'Database not set to PostgreSQL, but PostgreSQL is in use, something has gone terribly wrong';
if (typeof UserConfig.config.database.options != 'object') return 'PostgreSQL configuration missing';

let config = UserConfig.config.database.options;
Expand All @@ -34,7 +34,6 @@ export class PostgreSQLDatabase implements Database {
: undefined;

return issue;

}

public open(): Promise<void> {
Expand Down
Loading