forked from mirror/MCSManager
feat: added redis and asynchronous support
This commit is contained in:
parent
01b42b1040
commit
b97c976163
70
package-lock.json
generated
70
package-lock.json
generated
@ -218,6 +218,47 @@
|
||||
"make-plural": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"@redis/bloom": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.1.0.tgz",
|
||||
"integrity": "sha512-9QovlxmpRtvxVbN0UBcv8WfdSMudNZZTFqCsnBszcQXqaZb/TVe30ScgGEO7u1EAIacTPAo7/oCYjYAxiHLanQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@redis/client": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.4.2.tgz",
|
||||
"integrity": "sha512-oUdEjE0I7JS5AyaAjkD3aOXn9NhO7XKyPyXEyrgFDu++VrVBHUPnV6dgEya9TcMuj5nIJRuCzCm8ZP+c9zCHPw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"cluster-key-slot": "1.1.1",
|
||||
"generic-pool": "3.9.0",
|
||||
"yallist": "4.0.0"
|
||||
}
|
||||
},
|
||||
"@redis/graph": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.0.tgz",
|
||||
"integrity": "sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==",
|
||||
"optional": true
|
||||
},
|
||||
"@redis/json": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.4.tgz",
|
||||
"integrity": "sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==",
|
||||
"optional": true
|
||||
},
|
||||
"@redis/search": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.0.tgz",
|
||||
"integrity": "sha512-NyFZEVnxIJEybpy+YskjgOJRNsfTYqaPbK/Buv6W2kmFNaRk85JiqjJZA5QkRmWvGbyQYwoO5QfDi2wHskKrQQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@redis/time-series": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.4.tgz",
|
||||
"integrity": "sha512-ThUIgo2U/g7cCuZavucQTQzA9g9JbDDY2f64u3AbAoz/8vE2lt2U37LamDUVChhaDA3IRT9R6VvJwqnUfTJzng==",
|
||||
"optional": true
|
||||
},
|
||||
"@socket.io/component-emitter": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
|
||||
@ -1059,6 +1100,12 @@
|
||||
"shallow-clone": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"cluster-key-slot": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz",
|
||||
"integrity": "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==",
|
||||
"optional": true
|
||||
},
|
||||
"co": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||
@ -1709,6 +1756,12 @@
|
||||
"integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==",
|
||||
"dev": true
|
||||
},
|
||||
"generic-pool": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz",
|
||||
"integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==",
|
||||
"optional": true
|
||||
},
|
||||
"get-intrinsic": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz",
|
||||
@ -2738,6 +2791,20 @@
|
||||
"resolve": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"redis": {
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/redis/-/redis-4.5.1.tgz",
|
||||
"integrity": "sha512-oxXSoIqMJCQVBTfxP6BNTCtDMyh9G6Vi5wjdPdV/sRKkufyZslDqCScSGcOr6XGR/reAWZefz7E4leM31RgdBA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@redis/bloom": "1.1.0",
|
||||
"@redis/client": "1.4.2",
|
||||
"@redis/graph": "1.1.0",
|
||||
"@redis/json": "1.0.4",
|
||||
"@redis/search": "1.1.0",
|
||||
"@redis/time-series": "1.0.4"
|
||||
}
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||
@ -3470,8 +3537,7 @@
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"yeast": {
|
||||
"version": "0.1.2",
|
||||
|
@ -55,5 +55,8 @@
|
||||
"webpack": "^5.73.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-node-externals": "^3.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"redis": "^4.5.1"
|
||||
}
|
||||
}
|
||||
|
211
src/app.ts
211
src/app.ts
@ -6,32 +6,17 @@ import os from "os";
|
||||
import { $t, i18next } from "./app/i18n";
|
||||
import { initVersionManager, getVersion } from "./app/version";
|
||||
|
||||
// load global configuration file
|
||||
// Storage
|
||||
import RedisStorage from "./app/common/storage/redis_storage";
|
||||
import Storage from "./app/common/storage/sys_storage";
|
||||
|
||||
import { initSystemConfig, systemConfig } from "./app/setting";
|
||||
initSystemConfig();
|
||||
import SystemUser from "./app/service/system_user";
|
||||
import SystemRemoteService from "./app/service/system_remote_service";
|
||||
|
||||
initVersionManager();
|
||||
const VERSION = getVersion();
|
||||
|
||||
// show product Logo
|
||||
console.log(`______ _______________________ ___
|
||||
___ |/ /_ ____/_ ___/__ |/ /_____ _____________ _______ _____________
|
||||
__ /|_/ /_ / _____ \\__ /|_/ /_ __ \`/_ __ \\ __ \`/_ __ \`/ _ \\_ ___/
|
||||
_ / / / / /___ ____/ /_ / / / / /_/ /_ / / / /_/ /_ /_/ // __/ /
|
||||
/_/ /_/ \\____/ /____/ /_/ /_/ \\__,_/ /_/ /_/\\__,_/ _\\__, / \\___//_/
|
||||
/____/
|
||||
|
||||
+ Copyright (C) 2023 MCSManager <mcsmanager-dev@outlook.com>
|
||||
+ Version ${VERSION}
|
||||
`);
|
||||
|
||||
// Development environment detection before startup
|
||||
import fs from "fs";
|
||||
if (!fs.existsSync("public")) {
|
||||
console.log($t("app.developInfo"));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Http server requirements
|
||||
import Koa from "koa";
|
||||
import { v4 } from "uuid";
|
||||
import path from "path";
|
||||
@ -44,78 +29,11 @@ import open from "open";
|
||||
import { logger } from "./app/service/log";
|
||||
import { middleware as protocolMiddleware } from "./app/middleware/protocol";
|
||||
|
||||
const BASE_PATH = __dirname;
|
||||
|
||||
const app = new Koa();
|
||||
|
||||
// Listen for Koa errors
|
||||
app.on("error", (error) => {
|
||||
// Block all Koa framework level events
|
||||
// When Koa is attacked by a short connection flood, it is easy for error messages to swipe the screen, which may indirectly affect the operation of some applications
|
||||
});
|
||||
|
||||
app.use(
|
||||
koaBody({
|
||||
multipart: true,
|
||||
parsedMethods: ["POST", "PUT", "DELETE", "GET"]
|
||||
})
|
||||
);
|
||||
|
||||
app.keys = [v4()];
|
||||
app.use(
|
||||
session(
|
||||
{
|
||||
key: v4(),
|
||||
maxAge: 86400000,
|
||||
overwrite: true,
|
||||
httpOnly: true,
|
||||
signed: true,
|
||||
rolling: false,
|
||||
renew: false,
|
||||
secure: false
|
||||
},
|
||||
app
|
||||
)
|
||||
);
|
||||
|
||||
// Http log and print
|
||||
app.use(async (ctx, next) => {
|
||||
const ignoreUrls = ["/api/overview", "/api/files/status"];
|
||||
for (const iterator of ignoreUrls) {
|
||||
if (ctx.URL.pathname.includes(iterator)) return await next();
|
||||
}
|
||||
logger.info(`[HTTP] ${ctx.method}: ${ctx.URL.href}`);
|
||||
logger.info(`[HTTP] IP: ${ctx.ip} USER: ${ctx.session.userName} UUID: ${ctx.session.uuid}`);
|
||||
await next();
|
||||
});
|
||||
|
||||
// Protocol middleware
|
||||
app.use(protocolMiddleware);
|
||||
|
||||
// static file routing
|
||||
app.use(koaStatic(path.join(BASE_PATH, "public")));
|
||||
|
||||
// load all routes
|
||||
// Routes
|
||||
import { index } from "./app/index";
|
||||
|
||||
// Websocket routing (useless for now)
|
||||
// import SocketService from "./app/service/socket_service";
|
||||
index(app);
|
||||
|
||||
// Error reporting
|
||||
process.on("uncaughtException", function (err) {
|
||||
logger.error(`ERROR (uncaughtException):`, err);
|
||||
});
|
||||
|
||||
// Error reporting
|
||||
process.on("unhandledRejection", (reason, p) => {
|
||||
logger.error(`ERROR (unhandledRejection):`, reason, p);
|
||||
});
|
||||
|
||||
// start the HTTP service
|
||||
|
||||
function startUp(port: number, host?: string) {
|
||||
const httpServer = http.createServer(app.callback());
|
||||
function startUp(koaApp: Koa, port: number, host?: string) {
|
||||
const httpServer = http.createServer(koaApp.callback());
|
||||
|
||||
httpServer.on("error", (err) => {
|
||||
logger.error($t("app.httpSetupError"));
|
||||
@ -136,11 +54,11 @@ function startUp(port: number, host?: string) {
|
||||
logger.info("==================================");
|
||||
|
||||
if (os.platform() == "win32") {
|
||||
open(`http://localhost:${port}/`).then(() => {});
|
||||
open(`http://localhost:${port}/`).then(() => {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
startUp(systemConfig.httpPort, systemConfig.httpIp);
|
||||
|
||||
async function processExit() {
|
||||
try {
|
||||
@ -154,7 +72,110 @@ async function processExit() {
|
||||
}
|
||||
}
|
||||
|
||||
["SIGTERM", "SIGINT", "SIGQUIT"].forEach(function (sig) {
|
||||
(async () => {
|
||||
// load global configuration file
|
||||
initSystemConfig();
|
||||
|
||||
if (systemConfig.redisUrl.length != 0) {
|
||||
await RedisStorage.initialize(systemConfig.redisUrl);
|
||||
Storage.setStorageType(1);
|
||||
}
|
||||
|
||||
initVersionManager();
|
||||
const VERSION = getVersion();
|
||||
|
||||
// show product Logo
|
||||
console.log(`______ _______________________ ___
|
||||
___ |/ /_ ____/_ ___/__ |/ /_____ _____________ _______ _____________
|
||||
__ /|_/ /_ / _____ \\__ /|_/ /_ __ \`/_ __ \\ __ \`/_ __ \`/ _ \\_ ___/
|
||||
_ / / / / /___ ____/ /_ / / / / /_/ /_ / / / /_/ /_ /_/ // __/ /
|
||||
/_/ /_/ \\____/ /____/ /_/ /_/ \\__,_/ /_/ /_/\\__,_/ _\\__, / \\___//_/
|
||||
/____/
|
||||
|
||||
+ Copyright (C) 2023 MCSManager <mcsmanager-dev@outlook.com>
|
||||
+ Version ${VERSION}
|
||||
`);
|
||||
|
||||
// Development environment detection before startup
|
||||
if (!fs.existsSync(path.join(__dirname, "public"))) {
|
||||
console.log($t("app.developInfo"));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
await SystemUser.initialize();
|
||||
await SystemRemoteService.initialize();
|
||||
|
||||
const BASE_PATH = __dirname;
|
||||
|
||||
const app = new Koa();
|
||||
|
||||
// Listen for Koa errors
|
||||
app.on("error", (error) => {
|
||||
// Block all Koa framework level events
|
||||
// When Koa is attacked by a short connection flood, it is easy for error messages to swipe the screen, which may indirectly affect the operation of some applications
|
||||
});
|
||||
|
||||
app.use(
|
||||
koaBody({
|
||||
multipart: true,
|
||||
parsedMethods: ["POST", "PUT", "DELETE", "GET"]
|
||||
})
|
||||
);
|
||||
|
||||
app.keys = [v4()];
|
||||
app.use(
|
||||
session(
|
||||
{
|
||||
key: v4(),
|
||||
maxAge: 86400000,
|
||||
overwrite: true,
|
||||
httpOnly: true,
|
||||
signed: true,
|
||||
rolling: false,
|
||||
renew: false,
|
||||
secure: false
|
||||
},
|
||||
app
|
||||
)
|
||||
);
|
||||
|
||||
// Http log and print
|
||||
app.use(async (ctx, next) => {
|
||||
const ignoreUrls = ["/api/overview", "/api/files/status"];
|
||||
for (const iterator of ignoreUrls) {
|
||||
if (ctx.URL.pathname.includes(iterator)) return await next();
|
||||
}
|
||||
logger.info(`[HTTP] ${ctx.method}: ${ctx.URL.href}`);
|
||||
logger.info(`[HTTP] IP: ${ctx.ip} USER: ${ctx.session.userName} UUID: ${ctx.session.uuid}`);
|
||||
await next();
|
||||
});
|
||||
|
||||
// Protocol middleware
|
||||
app.use(protocolMiddleware);
|
||||
|
||||
// static file routing
|
||||
app.use(koaStatic(path.join(BASE_PATH, "public")));
|
||||
|
||||
// Websocket routing (useless for now)
|
||||
// import SocketService from "./app/service/socket_service";
|
||||
index(app);
|
||||
|
||||
// Error reporting
|
||||
process.on("uncaughtException", function(err) {
|
||||
logger.error(`ERROR (uncaughtException):`, err);
|
||||
});
|
||||
|
||||
// Error reporting
|
||||
process.on("unhandledRejection", (reason, p) => {
|
||||
logger.error(`ERROR (unhandledRejection):`, reason, p);
|
||||
});
|
||||
|
||||
// start the HTTP service
|
||||
|
||||
startUp(app, systemConfig.httpPort, systemConfig.httpIp);
|
||||
})();
|
||||
|
||||
["SIGTERM", "SIGINT", "SIGQUIT"].forEach(function(sig) {
|
||||
process.on(sig, () => {
|
||||
logger.warn(`${sig} close process signal detected.`);
|
||||
processExit();
|
||||
|
98
src/app/common/storage/file_storage.ts
Normal file
98
src/app/common/storage/file_storage.ts
Normal file
@ -0,0 +1,98 @@
|
||||
// Copyright (C) 2022 MCSManager <mcsmanager-dev@outlook.com>
|
||||
|
||||
import { $t } from "../../i18n";
|
||||
import path from "path";
|
||||
import fs from "fs-extra";
|
||||
import { IStorage } from "./storage_interface";
|
||||
|
||||
interface IClassz {
|
||||
name: string;
|
||||
}
|
||||
|
||||
class FileStorageSubsystem implements IStorage {
|
||||
public static readonly STIRAGE_DATA_PATH = path.normalize(path.join(process.cwd(), "data"));
|
||||
public static readonly STIRAGE_INDEX_PATH = path.normalize(path.join(process.cwd(), "data", "index"));
|
||||
|
||||
private checkFileName(name: string) {
|
||||
const blackList = ["\\", "/", ".."];
|
||||
for (const ch of blackList) {
|
||||
if (name.includes(ch)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stored in local file based on class definition and identifier
|
||||
*/
|
||||
public async store(category: string, uuid: string, object: any) {
|
||||
const dirPath = path.join(FileStorageSubsystem.STIRAGE_DATA_PATH, category);
|
||||
if (!fs.existsSync(dirPath)) fs.mkdirsSync(dirPath);
|
||||
if (!this.checkFileName(uuid)) throw new Error($t("common.uuidIrregular", { uuid: uuid }));
|
||||
const filePath = path.join(dirPath, `${uuid}.json`);
|
||||
const data = JSON.stringify(object, null, 4);
|
||||
fs.writeFileSync(filePath, data, { encoding: "utf-8" });
|
||||
}
|
||||
|
||||
// deep copy of the primitive type with the copy target as the prototype
|
||||
// target copy target object copy source
|
||||
protected defineAttr(target: any, object: any): any {
|
||||
for (const v of Object.keys(target)) {
|
||||
const objectValue = object[v];
|
||||
if (objectValue === undefined) continue;
|
||||
if (objectValue instanceof Array) {
|
||||
target[v] = objectValue;
|
||||
continue;
|
||||
}
|
||||
if (objectValue instanceof Object && typeof objectValue === "object") {
|
||||
this.defineAttr(target[v], objectValue);
|
||||
continue;
|
||||
}
|
||||
target[v] = objectValue;
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate an object based on the class definition and identifier
|
||||
*/
|
||||
public async load(category: string, classz: any, uuid: string) {
|
||||
const dirPath = path.join(FileStorageSubsystem.STIRAGE_DATA_PATH, category);
|
||||
if (!fs.existsSync(dirPath)) fs.mkdirsSync(dirPath);
|
||||
if (!this.checkFileName(uuid)) throw new Error($t("common.uuidIrregular", { uuid: uuid }));
|
||||
const filePath = path.join(dirPath, `${uuid}.json`);
|
||||
if (!fs.existsSync(filePath)) return null;
|
||||
const data = fs.readFileSync(filePath, { encoding: "utf-8" });
|
||||
const dataObject = JSON.parse(data);
|
||||
const target = new classz();
|
||||
// for (const v of Object. keys(target)) {
|
||||
// if (dataObject[v] !== undefined) target[v] = dataObject[v];
|
||||
// }
|
||||
// deep object copy
|
||||
return this.defineAttr(target, dataObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all identifiers related to this class through the class definition
|
||||
*/
|
||||
public async list(category: string) {
|
||||
const dirPath = path.join(FileStorageSubsystem.STIRAGE_DATA_PATH, category);
|
||||
if (!fs.existsSync(dirPath)) fs.mkdirsSync(dirPath);
|
||||
const files = fs.readdirSync(dirPath);
|
||||
const result = new Array<string>();
|
||||
files.forEach((name) => {
|
||||
result.push(name.replace(path.extname(name), ""));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an identifier instance of the specified type through the class definition
|
||||
*/
|
||||
public async delete(category: string, uuid: string) {
|
||||
const filePath = path.join(FileStorageSubsystem.STIRAGE_DATA_PATH, category, `${uuid}.json`);
|
||||
if (!fs.existsSync(filePath)) return;
|
||||
fs.removeSync(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
export default new FileStorageSubsystem();
|
121
src/app/common/storage/redis_storage.ts
Normal file
121
src/app/common/storage/redis_storage.ts
Normal file
@ -0,0 +1,121 @@
|
||||
// Copyright (C) 2023 MCSManager <mcsmanager-dev@outlook.com>
|
||||
|
||||
import { $t } from "../../i18n";
|
||||
import { createClient} from "redis";
|
||||
import { IStorage } from "./storage_interface";
|
||||
import Storage from "./sys_storage";
|
||||
import { logger } from "../../service/log";
|
||||
|
||||
class RedisStorageSubsystem implements IStorage {
|
||||
|
||||
private client = createClient()
|
||||
|
||||
private async connect() {
|
||||
try {
|
||||
await this.client.connect();
|
||||
} catch (e) {
|
||||
logger.error("Error occurred while trying to dial redis\n" + e);
|
||||
logger.warn("Due to an unrecoverable error, daemon will temporarily store data in files.")
|
||||
Storage.setStorageType(0)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Redis commands
|
||||
*/
|
||||
private async keys(param: string) {
|
||||
return await this.client.keys(param);
|
||||
}
|
||||
|
||||
private async set(key: string, value: string) {
|
||||
await this.client.set(key, value);
|
||||
}
|
||||
|
||||
private async get(key: string) {
|
||||
return await this.client.get(key);
|
||||
}
|
||||
|
||||
private async del(key: string) {
|
||||
await this.client.del(key);
|
||||
}
|
||||
|
||||
public async initialize(url: string) {
|
||||
this.client = createClient({ url: url });
|
||||
logger.info("Attempting to connect to redis...");
|
||||
await this.connect();
|
||||
logger.info("Connected to redis!");
|
||||
}
|
||||
|
||||
// Keep behavior same
|
||||
private checkFileName(name: string) {
|
||||
const blackList = ["\\", "/", ".."];
|
||||
for (const ch of blackList) {
|
||||
if (name.includes(ch)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stored in local file based on class definition and identifier
|
||||
*/
|
||||
public async store(category: string, uuid: string, object: any) {
|
||||
if (!this.checkFileName(uuid)) throw new Error($t("common.uuidIrregular", { uuid: uuid }));
|
||||
await this.set(category + ":" + uuid, JSON.stringify(object));
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate an object based on the class definition and identifier
|
||||
*/
|
||||
public async load(category: string, classz: any, uuid: string) {
|
||||
if (!this.checkFileName(uuid)) throw new Error($t("common.uuidIrregular", { uuid: uuid }));
|
||||
let result = await this.get(category + ":" + uuid)
|
||||
if (result == null) {
|
||||
return null;
|
||||
}
|
||||
const dataObject = JSON.parse(result);
|
||||
const target = new classz();
|
||||
return this.defineAttr(target, dataObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all identifiers related to this class through the class definition
|
||||
*/
|
||||
public async list(category: string) {
|
||||
let result: string[] = [];
|
||||
let m = Array<string>();
|
||||
result = await this.keys(category + "*")
|
||||
if (result != null && result.length != 0) {
|
||||
for (let i of result) m.push(i.replace(category + ":", ""));
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an identifier instance of the specified type through the class definition
|
||||
*/
|
||||
public async delete(category: string, uuid: string) {
|
||||
if (!this.checkFileName(uuid)) throw new Error($t("common.uuidIrregular", { uuid: uuid }));
|
||||
await this.del(category + ":" + uuid);
|
||||
}
|
||||
|
||||
// deep copy of the primitive type with the copy target as the prototype
|
||||
// target copy target object copy source
|
||||
protected defineAttr(target: any, object: any): any {
|
||||
for (const v of Object.keys(target)) {
|
||||
const objectValue = object[v];
|
||||
if (objectValue === undefined) continue;
|
||||
if (objectValue instanceof Array) {
|
||||
target[v] = objectValue;
|
||||
continue;
|
||||
}
|
||||
if (objectValue instanceof Object && typeof objectValue === "object") {
|
||||
this.defineAttr(target[v], objectValue);
|
||||
continue;
|
||||
}
|
||||
target[v] = objectValue;
|
||||
}
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
export default new RedisStorageSubsystem();
|
6
src/app/common/storage/storage_interface.ts
Normal file
6
src/app/common/storage/storage_interface.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface IStorage {
|
||||
store(category: string, uuid: string, object: any): Promise<void>;
|
||||
load(category: string, classz: any, uuid: string): Promise<any>;
|
||||
list(category: string): Promise<Array<string>>;
|
||||
delete(category: string, uuid: string): Promise<void>;
|
||||
}
|
33
src/app/common/storage/sys_storage.ts
Normal file
33
src/app/common/storage/sys_storage.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { IStorage } from "./storage_interface";
|
||||
import file_storage from "./file_storage";
|
||||
import redis_storage from "./redis_storage";
|
||||
|
||||
class SystemStorage {
|
||||
private exclusions = ["Config", "SystemConfig"]; // Config is from daemon and SystemConfig is from panel
|
||||
private storageSystem: IStorage = file_storage;
|
||||
|
||||
public setStorageType(type: number) {
|
||||
switch (type) {
|
||||
case 0: {
|
||||
// Using fs-based storage
|
||||
this.storageSystem = file_storage;
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
// Using redis-based storage
|
||||
this.storageSystem = redis_storage;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
this.storageSystem = file_storage;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getStorage() {
|
||||
return this.storageSystem;
|
||||
}
|
||||
}
|
||||
|
||||
export default new SystemStorage();
|
@ -34,4 +34,5 @@ export default class SystemConfig {
|
||||
|
||||
// Quick installation address for the Chinese market
|
||||
quickInstallAddr = "https://mcsmanager.oss-cn-guangzhou.aliyuncs.com/quick_install.json";
|
||||
redisUrl = "";
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ export class DataStructure {
|
||||
if (!fs.existsSync(this.__filename__)) return;
|
||||
let data = fs.readFileSync(this.__filename__, "utf-8");
|
||||
let ele = JSON.parse(data);
|
||||
for (var key in ele) {
|
||||
for (const key in ele) {
|
||||
this[key] = ele[key];
|
||||
}
|
||||
}
|
||||
|
@ -21,8 +21,7 @@ router.post(
|
||||
const permission = Number(ctx.request.body.permission);
|
||||
if (!userSystem.validatePassword(passWord)) throw new Error($t("router.user.invalidPassword"));
|
||||
if (userSystem.existUserName(userName)) throw new Error($t("router.user.existsUserName"));
|
||||
const result = register(ctx, userName, passWord, permission);
|
||||
ctx.body = result;
|
||||
ctx.body = await register(ctx, userName, passWord, permission);
|
||||
}
|
||||
);
|
||||
|
||||
@ -31,7 +30,7 @@ router.del("/", permission({ level: 10 }), async (ctx: Koa.ParameterizedContext)
|
||||
const uuids = ctx.request.body;
|
||||
try {
|
||||
for (const iterator of uuids) {
|
||||
userSystem.deleteInstance(iterator);
|
||||
await userSystem.deleteInstance(iterator);
|
||||
}
|
||||
ctx.body = true;
|
||||
} catch (error) {
|
||||
|
@ -100,7 +100,7 @@ router.post(
|
||||
async (ctx) => {
|
||||
const parameter = ctx.request.body;
|
||||
// do asynchronous registration
|
||||
const instance = RemoteServiceSubsystem.registerRemoteService({
|
||||
const instance = await RemoteServiceSubsystem.registerRemoteService({
|
||||
apiKey: parameter.apiKey,
|
||||
port: parameter.port,
|
||||
ip: parameter.ip,
|
||||
|
@ -16,7 +16,7 @@ router.put("/", permission({ level: 10 }), async (ctx: Koa.ParameterizedContext)
|
||||
if (passWord && !userSystem.validatePassword(passWord))
|
||||
throw new Error($t("router.user.passwordCheck"));
|
||||
try {
|
||||
userSystem.edit(uuid, config);
|
||||
await userSystem.edit(uuid, config);
|
||||
ctx.body = true;
|
||||
} catch (error) {
|
||||
ctx.throw(500, error.message);
|
||||
|
@ -1,13 +1,9 @@
|
||||
// Copyright (C) 2022 MCSManager <mcsmanager-dev@outlook.com>
|
||||
|
||||
import Koa from "koa";
|
||||
import Router from "@koa/router";
|
||||
import permission from "../../middleware/permission";
|
||||
import validator from "../../middleware/validator";
|
||||
import { check, login, logout, register, getUserUuid } from "../../service/passport_service";
|
||||
import userSystem from "../../service/system_user";
|
||||
import { ICompleteUser } from "../../entity/entity_interface";
|
||||
import { getToken, isAjax } from "../../service/passport_service";
|
||||
import { getUserUuid } from "../../service/passport_service";
|
||||
import RemoteServiceSubsystem from "../../service/system_remote_service";
|
||||
import RemoteRequest from "../../service/remote_command";
|
||||
import { isHaveInstanceByUuid } from "../../service/permission_service";
|
||||
@ -45,11 +41,10 @@ router.put(
|
||||
stopCommand: String(config.stopCommand)
|
||||
};
|
||||
const remoteService = RemoteServiceSubsystem.getInstance(serviceUuid);
|
||||
const result = await new RemoteRequest(remoteService).request("instance/update", {
|
||||
ctx.body = await new RemoteRequest(remoteService).request("instance/update", {
|
||||
instanceUuid,
|
||||
config: believableConfig
|
||||
});
|
||||
ctx.body = result;
|
||||
} catch (err) {
|
||||
ctx.body = err;
|
||||
}
|
||||
|
@ -123,7 +123,7 @@ router.put(
|
||||
const config = ctx.request.body;
|
||||
const { passWord, isInit } = config;
|
||||
if (!userSystem.validatePassword(passWord)) throw new Error($t("router.user.passwordCheck"));
|
||||
userSystem.edit(userUuid, { passWord, isInit });
|
||||
await userSystem.edit(userUuid, { passWord, isInit });
|
||||
ctx.body = true;
|
||||
}
|
||||
}
|
||||
@ -144,11 +144,11 @@ router.put(
|
||||
if (user) {
|
||||
if (enable) {
|
||||
newKey = v4().replace(/-/gim, "");
|
||||
userSystem.edit(userUuid, {
|
||||
await userSystem.edit(userUuid, {
|
||||
apiKey: newKey
|
||||
});
|
||||
} else {
|
||||
userSystem.edit(userUuid, {
|
||||
await userSystem.edit(userUuid, {
|
||||
apiKey: ""
|
||||
});
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ router.all(
|
||||
if (userSystem.objects.size === 0) {
|
||||
if (!userSystem.validatePassword(passWord)) throw new Error($t("router.user.passwordCheck"));
|
||||
logger.info($t("router.login.init", { userName }));
|
||||
userSystem.create({
|
||||
await userSystem.create({
|
||||
userName,
|
||||
passWord,
|
||||
permission: 10
|
||||
|
@ -59,7 +59,7 @@ export function logout(ctx: Koa.ParameterizedContext): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function register(
|
||||
export async function register(
|
||||
ctx: Koa.ParameterizedContext,
|
||||
userName: string,
|
||||
passWord: string,
|
||||
@ -72,7 +72,7 @@ export function register(
|
||||
});
|
||||
if (f) {
|
||||
// Next. UUID and other data will be automatically generated.
|
||||
userSystem.create({
|
||||
await userSystem.create({
|
||||
userName,
|
||||
passWord,
|
||||
permission
|
||||
|
@ -4,22 +4,20 @@ import { logger } from "./log";
|
||||
import { IRemoteService, RemoteServiceConfig } from "../entity/entity_interface";
|
||||
import RemoteService from "../entity/remote_service";
|
||||
import { UniversalRemoteSubsystem } from "./base/urs";
|
||||
import StorageSubsystem from "../common/system_storage";
|
||||
import Storage from "../common/storage/sys_storage";
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import { $t } from "../i18n";
|
||||
import RemoteRequest from "./remote_command";
|
||||
|
||||
// The RemoteServiceSubsystem will be one of the most important systems
|
||||
// main function is to store remote services everywhere
|
||||
// Scan local services, unified management, remote calls and proxies, etc.
|
||||
class RemoteServiceSubsystem extends UniversalRemoteSubsystem<RemoteService> {
|
||||
constructor() {
|
||||
super();
|
||||
async initialize() {
|
||||
// If it is the first startup, it will automatically try to connect to "LocalHost",
|
||||
// otherwise it will automatically read from the configuration file and connect to all remote services.
|
||||
StorageSubsystem.list("RemoteServiceConfig").forEach((uuid) => {
|
||||
const config = StorageSubsystem.load(
|
||||
for (const uuid of (await Storage.getStorage().list("RemoteServiceConfig"))) {
|
||||
const config = await Storage.getStorage().load(
|
||||
"RemoteServiceConfig",
|
||||
RemoteServiceConfig,
|
||||
uuid
|
||||
@ -27,11 +25,11 @@ class RemoteServiceSubsystem extends UniversalRemoteSubsystem<RemoteService> {
|
||||
const newService = new RemoteService(uuid, config);
|
||||
this.setInstance(uuid, newService);
|
||||
newService.connect();
|
||||
});
|
||||
}
|
||||
|
||||
// If there is no daemon process, check whether there is a daemon process locally
|
||||
if (this.services.size === 0) {
|
||||
this.initConnectLocalhost("");
|
||||
await this.initConnectLocalhost("");
|
||||
}
|
||||
|
||||
logger.info($t("systemRemoteService.nodeCount", { n: this.services.size }));
|
||||
@ -46,49 +44,49 @@ class RemoteServiceSubsystem extends UniversalRemoteSubsystem<RemoteService> {
|
||||
// apiKey: "test_key",
|
||||
// port: 24444
|
||||
// });
|
||||
registerRemoteService(config: IRemoteService) {
|
||||
const instance = this.newInstance(config);
|
||||
StorageSubsystem.store("RemoteServiceConfig", instance.uuid, instance.config);
|
||||
async registerRemoteService(config: IRemoteService) {
|
||||
const instance = await this.newInstance(config);
|
||||
await Storage.getStorage().store("RemoteServiceConfig", instance.uuid, instance.config);
|
||||
instance.connect();
|
||||
return instance;
|
||||
}
|
||||
|
||||
// Delete the specified remote service based on UUID
|
||||
deleteRemoteService(uuid: string) {
|
||||
async deleteRemoteService(uuid: string) {
|
||||
if (this.getInstance(uuid)) {
|
||||
this.getInstance(uuid).disconnect();
|
||||
this.deleteInstance(uuid);
|
||||
StorageSubsystem.delete("RemoteServiceConfig", uuid);
|
||||
await Storage.getStorage().delete("RemoteServiceConfig", uuid);
|
||||
}
|
||||
}
|
||||
|
||||
// According to the IRemoteService, New a RemoteService object
|
||||
// Used to initialize objects.
|
||||
newInstance(config: IRemoteService) {
|
||||
async newInstance(config: IRemoteService) {
|
||||
const instance = new RemoteService(
|
||||
config.uuid || this.randdomUuid(),
|
||||
new RemoteServiceConfig()
|
||||
);
|
||||
this.setInstance(instance.uuid, instance);
|
||||
this.edit(instance.uuid, config);
|
||||
await this.edit(instance.uuid, config);
|
||||
return instance;
|
||||
}
|
||||
|
||||
// Edit the configuration file of the instance
|
||||
edit(uuid: string, config: IRemoteService) {
|
||||
async edit(uuid: string, config: IRemoteService) {
|
||||
const instance = this.getInstance(uuid);
|
||||
if (config.remarks) instance.config.remarks = config.remarks;
|
||||
if (config.ip) instance.config.ip = config.ip;
|
||||
if (config.port) instance.config.port = config.port;
|
||||
if (config.apiKey) instance.config.apiKey = config.apiKey;
|
||||
StorageSubsystem.store("RemoteServiceConfig", instance.uuid, instance.config);
|
||||
await Storage.getStorage().store("RemoteServiceConfig", instance.uuid, instance.config);
|
||||
}
|
||||
|
||||
// Scannce localhost service
|
||||
// First use, need to scan the local host
|
||||
// Note: Every time you execute "initConnectLocalhost",
|
||||
// it will be managed by the subsystem (regardless of whether the target exists).
|
||||
async initConnectLocalhost(key?: string) {
|
||||
async initConnectLocalhost(key?: string): Promise<RemoteService> {
|
||||
const ip = "localhost";
|
||||
const localKeyFilePath = path.normalize(
|
||||
path.join(process.cwd(), "../daemon/data/Config/global.json")
|
||||
@ -101,10 +99,10 @@ class RemoteServiceSubsystem extends UniversalRemoteSubsystem<RemoteService> {
|
||||
);
|
||||
const localKey = localDaemonConfig.key;
|
||||
const localPort = localDaemonConfig.port;
|
||||
return this.registerRemoteService({ apiKey: localKey, port: localPort, ip });
|
||||
return await this.registerRemoteService({ apiKey: localKey, port: localPort, ip });
|
||||
} else if (key) {
|
||||
const port = 24444;
|
||||
return this.registerRemoteService({ apiKey: key, port, ip });
|
||||
return await this.registerRemoteService({ apiKey: key, port, ip });
|
||||
}
|
||||
logger.warn($t("systemRemoteService.error"));
|
||||
|
||||
|
@ -5,7 +5,7 @@ import { v4 } from "uuid";
|
||||
import { IUserApp, User, UserPassWordType } from "../entity/user";
|
||||
import { logger } from "./log";
|
||||
import { IUser } from "../entity/entity_interface";
|
||||
import StorageSubsystem from "../common/system_storage";
|
||||
import Storage from "../common/storage/sys_storage";
|
||||
import { QueryWrapper, LocalFileSource } from "../common/query_wrapper";
|
||||
import { $t } from "../i18n";
|
||||
import bcrypt from "bcryptjs";
|
||||
@ -13,15 +13,15 @@ import bcrypt from "bcryptjs";
|
||||
class UserSubsystem {
|
||||
public readonly objects: Map<string, User> = new Map();
|
||||
|
||||
constructor() {
|
||||
StorageSubsystem.list("User").forEach((uuid) => {
|
||||
const user = StorageSubsystem.load("User", User, uuid) as User;
|
||||
async initialize() {
|
||||
for (const uuid of (await Storage.getStorage().list("User"))) {
|
||||
const user = await Storage.getStorage().load("User", User, uuid) as User;
|
||||
this.objects.set(uuid, user);
|
||||
});
|
||||
}
|
||||
logger.info($t("systemUser.userCount", { n: this.objects.size }));
|
||||
}
|
||||
|
||||
create(config: IUser): User {
|
||||
async create(config: IUser): Promise<User> {
|
||||
const newUuid = v4().replace(/-/gim, "");
|
||||
// Initialize necessary user data
|
||||
const instance = new User();
|
||||
@ -29,13 +29,13 @@ class UserSubsystem {
|
||||
instance.registerTime = new Date().toLocaleString();
|
||||
// add to the user system
|
||||
this.setInstance(newUuid, instance);
|
||||
this.edit(instance.uuid, config);
|
||||
await this.edit(instance.uuid, config);
|
||||
// Persistently save user information
|
||||
StorageSubsystem.store("User", instance.uuid, instance);
|
||||
await Storage.getStorage().store("User", instance.uuid, instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
edit(uuid: string, config: any) {
|
||||
async edit(uuid: string, config: any) {
|
||||
const instance = this.getInstance(uuid);
|
||||
if (config.userName) instance.userName = config.userName;
|
||||
if (config.isInit != null) instance.isInit = Boolean(config.isInit);
|
||||
@ -48,7 +48,7 @@ class UserSubsystem {
|
||||
}
|
||||
if (config.instances) this.setUserInstances(uuid, config.instances);
|
||||
if (config.apiKey != null) instance.apiKey = config.apiKey;
|
||||
StorageSubsystem.store("User", uuid, instance);
|
||||
await Storage.getStorage().store("User", uuid, instance);
|
||||
}
|
||||
|
||||
validatePassword(password = "") {
|
||||
@ -113,10 +113,10 @@ class UserSubsystem {
|
||||
return this.objects.has(uuid);
|
||||
}
|
||||
|
||||
deleteInstance(uuid: string) {
|
||||
async deleteInstance(uuid: string) {
|
||||
if (this.hasInstance(uuid)) {
|
||||
this.objects.delete(uuid);
|
||||
StorageSubsystem.delete("User", uuid);
|
||||
await Storage.getStorage().delete("User", uuid);
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,4 +125,4 @@ class UserSubsystem {
|
||||
}
|
||||
}
|
||||
|
||||
export = new UserSubsystem();
|
||||
export default new UserSubsystem();
|
||||
|
@ -6,7 +6,6 @@
|
||||
"esModuleInterop": true,
|
||||
"allowJs": false,
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES2018",
|
||||
"noImplicitAny": true,
|
||||
|
Loading…
Reference in New Issue
Block a user